test: exception safety for sparse set and storage

This commit is contained in:
Michele Caini
2021-05-31 17:12:40 +02:00
parent c28f52b816
commit b8bc3e4e94
7 changed files with 491 additions and 52 deletions

View File

@@ -47,20 +47,18 @@ class basic_sparse_set {
using traits_type = entt_traits<Entity>;
using alloc_type = typename std::allocator_traits<Allocator>::template rebind_alloc<Entity>;
using alloc_traits = std::allocator_traits<alloc_type>;
using alloc_traits = typename std::allocator_traits<Allocator>::template rebind_traits<Entity>;
using alloc_pointer = typename alloc_traits::pointer;
using alloc_const_pointer = typename alloc_traits::const_pointer;
using bucket_alloc_type = typename std::allocator_traits<Allocator>::template rebind_alloc<alloc_pointer>;
using bucket_alloc_traits = std::allocator_traits<bucket_alloc_type>;
using bucket_alloc_traits = typename std::allocator_traits<Allocator>::template rebind_traits<alloc_pointer>;
using bucket_alloc_pointer = typename bucket_alloc_traits::pointer;
static_assert(alloc_traits::propagate_on_container_move_assignment::value);
static_assert(bucket_alloc_traits::propagate_on_container_move_assignment::value);
class sparse_set_iterator final {
friend class basic_sparse_set<Entity>;
friend class basic_sparse_set<Entity, Allocator>;
using index_type = typename traits_type::difference_type;
@@ -189,7 +187,7 @@ class basic_sparse_set {
std::destroy(sparse, sparse + bucket);
bucket_alloc_traits::deallocate(bucket_allocator, sparse, bucket);
sparse = mem;
bucket = sz;
}
@@ -302,7 +300,7 @@ protected:
public:
/*! @brief Allocator type. */
using allocator_type = alloc_type;
using allocator_type = typename alloc_traits::allocator_type;
/*! @brief Underlying entity identifier. */
using entity_type = Entity;
/*! @brief Unsigned integer type. */
@@ -450,7 +448,7 @@ public:
* @return An iterator to the first entity of the internal packed array.
*/
[[nodiscard]] iterator begin() const ENTT_NOEXCEPT {
return iterator{&packed, static_cast<typename traits_type::difference_type>(count)};
return iterator{std::addressof(packed), static_cast<typename traits_type::difference_type>(count)};
}
/**
@@ -464,7 +462,7 @@ public:
* internal packed array.
*/
[[nodiscard]] iterator end() const ENTT_NOEXCEPT {
return iterator{&packed, {}};
return iterator{std::addressof(packed), {}};
}
/**
@@ -785,8 +783,8 @@ public:
}
private:
alloc_type allocator;
bucket_alloc_type bucket_allocator;
typename alloc_traits::allocator_type allocator;
typename bucket_alloc_traits::allocator_type bucket_allocator;
bucket_alloc_pointer sparse;
alloc_pointer packed;
std::size_t bucket;

View File

@@ -53,29 +53,25 @@ class basic_storage: public basic_sparse_set<Entity, typename std::allocator_tra
static constexpr auto packed_page = ENTT_PACKED_PAGE;
using underlying_type = basic_sparse_set<Entity>;
using underlying_type = basic_sparse_set<Entity, typename std::allocator_traits<Allocator>::template rebind_alloc<Entity>>;
using traits_type = entt_traits<Entity>;
using alloc_type = typename std::allocator_traits<Allocator>::template rebind_alloc<Type>;
using alloc_traits = std::allocator_traits<alloc_type>;
using alloc_traits = typename std::allocator_traits<Allocator>::template rebind_traits<Type>;
using alloc_pointer = typename alloc_traits::pointer;
using alloc_const_pointer = typename alloc_traits::const_pointer;
using bucket_alloc_type = typename std::allocator_traits<Allocator>::template rebind_alloc<alloc_pointer>;
using bucket_alloc_traits = std::allocator_traits<bucket_alloc_type>;
using bucket_alloc_traits = typename std::allocator_traits<Allocator>::template rebind_traits<alloc_pointer>;
using bucket_alloc_pointer = typename bucket_alloc_traits::pointer;
using bucket_alloc_const_type = typename std::allocator_traits<Allocator>::template rebind_alloc<alloc_const_pointer>;
using bucket_alloc_const_pointer = typename std::allocator_traits<bucket_alloc_const_type>::const_pointer;
using entity_alloc_type = typename std::allocator_traits<Allocator>::template rebind_alloc<Entity>;
static_assert(alloc_traits::propagate_on_container_move_assignment::value);
static_assert(bucket_alloc_traits::propagate_on_container_move_assignment::value);
template<typename Value>
class storage_iterator final {
friend class basic_storage<Entity, Type>;
friend class basic_storage<Entity, Type, Allocator>;
storage_iterator(bucket_alloc_pointer const *ref, const typename traits_type::difference_type idx) ENTT_NOEXCEPT
: packed{ref}, index{idx}
@@ -211,7 +207,7 @@ class basic_storage: public basic_sparse_set<Entity, typename std::allocator_tra
bucket_alloc_traits::deallocate(bucket_allocator, mem, length);
ENTT_THROW;
}
for(auto pos = length; pos < bucket; ++pos) {
alloc_traits::deallocate(allocator, packed[pos], packed_page);
}
@@ -295,7 +291,7 @@ protected:
public:
/*! @brief Allocator type. */
using allocator_type = alloc_type;
using allocator_type = typename alloc_traits::allocator_type;
/*! @brief Type of the objects assigned to entities. */
using value_type = Type;
/*! @brief Underlying entity identifier. */
@@ -320,7 +316,7 @@ public:
* @param alloc Allocator to use (possibly default-constructed).
*/
explicit basic_storage(const allocator_type &alloc = {})
: basic_sparse_set<entity_type, entity_alloc_type>{alloc},
: underlying_type{alloc},
allocator{alloc},
bucket_allocator{alloc},
packed{},
@@ -332,7 +328,7 @@ public:
* @param other The instance to move from.
*/
basic_storage(basic_storage &&other) ENTT_NOEXCEPT
: basic_sparse_set<entity_type, entity_alloc_type>{std::move(other)},
: underlying_type{std::move(other)},
allocator{std::move(other.allocator)},
bucket_allocator{std::move(other.bucket_allocator)},
packed{std::exchange(other.packed, bucket_alloc_pointer{})},
@@ -352,7 +348,7 @@ public:
basic_storage & operator=(basic_storage &&other) ENTT_NOEXCEPT {
release_memory();
basic_sparse_set<entity_type, entity_alloc_type>::operator=(std::move(other));
underlying_type::operator=(std::move(other));
allocator = std::move(other.allocator);
bucket_allocator = std::move(other.bucket_allocator);
@@ -416,7 +412,7 @@ public:
*/
[[nodiscard]] const_iterator cbegin() const ENTT_NOEXCEPT {
const typename traits_type::difference_type pos = underlying_type::size();
return const_iterator{&packed, pos};
return const_iterator{std::addressof(packed), pos};
}
/*! @copydoc cbegin */
@@ -427,7 +423,7 @@ public:
/*! @copydoc begin */
[[nodiscard]] iterator begin() ENTT_NOEXCEPT {
const typename traits_type::difference_type pos = underlying_type::size();
return iterator{&packed, pos};
return iterator{std::addressof(packed), pos};
}
/**
@@ -441,7 +437,7 @@ public:
* internal array.
*/
[[nodiscard]] const_iterator cend() const ENTT_NOEXCEPT {
return const_iterator{&packed, {}};
return const_iterator{std::addressof(packed), {}};
}
/*! @copydoc cend */
@@ -451,7 +447,7 @@ public:
/*! @copydoc end */
[[nodiscard]] iterator end() ENTT_NOEXCEPT {
return iterator{&packed, {}};
return iterator{std::addressof(packed), {}};
}
/**
@@ -693,8 +689,8 @@ public:
}
private:
alloc_type allocator;
bucket_alloc_type bucket_allocator;
typename alloc_traits::allocator_type allocator;
typename bucket_alloc_traits::allocator_type bucket_allocator;
bucket_alloc_pointer packed;
std::size_t bucket;
};
@@ -703,7 +699,7 @@ private:
/*! @copydoc basic_storage */
template<typename Entity, typename Type, typename Allocator>
class basic_storage<Entity, Type, Allocator, std::enable_if_t<is_empty_v<Type>>>: public basic_sparse_set<Entity, typename std::allocator_traits<Allocator>::template rebind_alloc<Entity>> {
using underlying_type = basic_sparse_set<Entity>;
using underlying_type = basic_sparse_set<Entity, typename std::allocator_traits<Allocator>::template rebind_alloc<Entity>>;
public:
/*! @brief Type of the objects assigned to entities. */

View File

@@ -7,6 +7,8 @@
#include <gtest/gtest.h>
#include <entt/entity/sparse_set.hpp>
#include <entt/entity/fwd.hpp>
#include "throwing_allocator.hpp"
#include "throwing_entity.hpp"
struct empty_type {};
struct boxed_int { int value; };
@@ -580,3 +582,78 @@ TEST(SparseSet, CanModifyDuringIteration) {
const auto entity = *it;
(void)entity;
}
TEST(SparseSet, ThrowingEntity) {
entt::basic_sparse_set<test::throwing_entity> set{};
test::throwing_entity::trigger_on_entity = 0u;
// strong exception safety
ASSERT_THROW(set.emplace(42), typename test::throwing_entity::exception_type);
ASSERT_TRUE(set.empty());
test::throwing_entity::trigger_on_entity = 42u;
const test::throwing_entity entities[2u]{42, 1};
// basic exception safety
ASSERT_THROW(set.insert(std::begin(entities), std::end(entities)), typename test::throwing_entity::exception_type);
ASSERT_EQ(set.size(), 0u);
ASSERT_FALSE(set.contains(1));
// basic exception safety
ASSERT_THROW(set.insert(std::rbegin(entities), std::rend(entities)), typename test::throwing_entity::exception_type);
ASSERT_EQ(set.size(), 1u);
ASSERT_TRUE(set.contains(1));
}
TEST(SparseSet, ThrowingAllocator) {
entt::basic_sparse_set<entt::entity, test::throwing_allocator<entt::entity>> set{};
test::throwing_allocator<entt::entity>::trigger_on_allocate = true;
// strong exception safety
ASSERT_THROW(set.reserve(1u), test::throwing_allocator<entt::entity>::exception_type);
ASSERT_EQ(set.capacity(), 0u);
ASSERT_EQ(set.extent(), 0u);
set.emplace(entt::entity{0});
test::throwing_allocator<entt::entity>::trigger_on_allocate = true;
// strong exception safety
ASSERT_THROW(set.reserve(2u), test::throwing_allocator<entt::entity>::exception_type);
ASSERT_EQ(set.capacity(), 1u);
ASSERT_EQ(set.extent(), ENTT_SPARSE_PAGE);
ASSERT_TRUE(set.contains(entt::entity{0}));
test::throwing_allocator<entt::entity>::trigger_on_pointer_copy = true;
// strong exception safety
ASSERT_THROW(set.reserve(2u), test::throwing_allocator<entt::entity>::exception_type);
ASSERT_EQ(set.capacity(), 1u);
ASSERT_EQ(set.extent(), ENTT_SPARSE_PAGE);
ASSERT_TRUE(set.contains(entt::entity{0}));
set.reserve(ENTT_PACKED_PAGE);
test::throwing_allocator<entt::entity>::trigger_on_pointer_copy = true;
// strong exception safety
ASSERT_THROW(set.emplace(entt::entity{ENTT_SPARSE_PAGE + 1u}), test::throwing_allocator<entt::entity>::exception_type);
ASSERT_EQ(set.capacity(), ENTT_PACKED_PAGE);
ASSERT_EQ(set.extent(), ENTT_SPARSE_PAGE);
ASSERT_TRUE(set.contains(entt::entity{0}));
// unnecessary but they test a bit of template machinery :)
set.clear();
set.shrink_to_fit();
set = decltype(set){};
set.reserve(ENTT_PACKED_PAGE);
set.emplace(entt::entity{ENTT_SPARSE_PAGE + 1u});
test::throwing_allocator<entt::entity>::trigger_on_pointer_copy = true;
// strong exception safety
ASSERT_THROW(set.emplace(entt::entity{0}), test::throwing_allocator<entt::entity>::exception_type);
ASSERT_EQ(set.capacity(), ENTT_PACKED_PAGE);
ASSERT_EQ(set.extent(), 2 * ENTT_SPARSE_PAGE);
ASSERT_FALSE(set.contains(entt::entity{0}));
ASSERT_TRUE(set.contains(entt::entity{ENTT_SPARSE_PAGE + 1u}));
}

View File

@@ -7,6 +7,9 @@
#include <gtest/gtest.h>
#include <entt/entity/storage.hpp>
#include <entt/entity/fwd.hpp>
#include "throwing_allocator.hpp"
#include "throwing_component.hpp"
#include "throwing_entity.hpp"
struct empty_type {};
struct boxed_int { int value; };
@@ -15,15 +18,6 @@ bool operator==(const boxed_int &lhs, const boxed_int &rhs) {
return lhs.value == rhs.value;
}
struct throwing_component {
struct constructor_exception: std::exception {};
[[noreturn]] throwing_component() { throw constructor_exception{}; }
// necessary to disable the empty type optimization
int data;
};
struct update_from_destructor {
~update_from_destructor() {
if(target != entt::null) {
@@ -760,18 +754,6 @@ TEST(Storage, MoveOnlyComponent) {
(void)pool;
}
TEST(Storage, EmplaceStrongExceptionGuarantee) {
entt::storage<throwing_component> pool;
try {
pool.emplace(entt::entity{0});
} catch (const throwing_component::constructor_exception &) {
ASSERT_TRUE(pool.empty());
}
ASSERT_TRUE(pool.empty());
}
TEST(Storage, UpdateFromDestructor) {
static constexpr auto size = 10u;
@@ -796,3 +778,118 @@ TEST(Storage, UpdateFromDestructor) {
test(entt::entity(size - 1u));
test(entt::entity{0u});
}
TEST(Storage, ThrowingEntity) {
entt::basic_storage<test::throwing_entity, int> pool;
test::throwing_entity::trigger_on_entity = 42u;
// strong exception safety
ASSERT_THROW(pool.emplace(42, 0), typename test::throwing_entity::exception_type);
ASSERT_TRUE(pool.empty());
const test::throwing_entity entities[2u]{42, 1};
const int components[2u]{42, 1};
// basic exception safety
ASSERT_THROW(pool.insert(std::begin(entities), std::end(entities), 1), typename test::throwing_entity::exception_type);
ASSERT_EQ(pool.size(), 0u);
ASSERT_FALSE(pool.contains(1));
// basic exception safety
ASSERT_THROW(pool.insert(std::rbegin(entities), std::rend(entities), 1), typename test::throwing_entity::exception_type);
ASSERT_EQ(pool.size(), 1u);
ASSERT_TRUE(pool.contains(1));
ASSERT_EQ(pool.get(1), 1);
pool.clear();
// basic exception safety
ASSERT_THROW(pool.insert(std::begin(entities), std::end(entities), std::begin(components)), typename test::throwing_entity::exception_type);
ASSERT_EQ(pool.size(), 0u);
ASSERT_FALSE(pool.contains(1));
// basic exception safety
ASSERT_THROW(pool.insert(std::rbegin(entities), std::rend(entities), std::rbegin(components)), typename test::throwing_entity::exception_type);
ASSERT_EQ(pool.size(), 1u);
ASSERT_TRUE(pool.contains(1));
ASSERT_EQ(pool.get(1), 1);
}
TEST(Storage, ThrowingComponent) {
entt::storage<test::throwing_component> pool;
test::throwing_component::trigger_on_value = 42;
// strong exception safety
ASSERT_THROW(pool.emplace(entt::entity{0}, test::throwing_component{42}), typename test::throwing_component::exception_type);
ASSERT_TRUE(pool.empty());
const entt::entity entities[2u]{entt::entity{42}, entt::entity{1}};
const test::throwing_component components[2u]{42, 1};
// basic exception safety
ASSERT_THROW(pool.insert(std::begin(entities), std::end(entities), test::throwing_component{42}), typename test::throwing_component::exception_type);
ASSERT_EQ(pool.size(), 0u);
ASSERT_FALSE(pool.contains(entt::entity{1}));
// basic exception safety
ASSERT_THROW(pool.insert(std::begin(entities), std::end(entities), std::begin(components)), typename test::throwing_component::exception_type);
ASSERT_EQ(pool.size(), 0u);
ASSERT_FALSE(pool.contains(entt::entity{1}));
// basic exception safety
ASSERT_THROW(pool.insert(std::rbegin(entities), std::rend(entities), std::rbegin(components)), typename test::throwing_component::exception_type);
ASSERT_EQ(pool.size(), 1u);
ASSERT_TRUE(pool.contains(entt::entity{1}));
ASSERT_EQ(pool.get(entt::entity{1}), 1);
pool.clear();
pool.emplace(entt::entity{1}, 1);
pool.emplace(entt::entity{42}, 42);
ASSERT_THROW(pool.erase(entt::entity{1}), typename test::throwing_component::exception_type);
ASSERT_FALSE(pool.empty());
ASSERT_EQ(pool.size(), 1u);
ASSERT_TRUE(pool.contains(entt::entity{42}));
ASSERT_FALSE(pool.contains(entt::entity{1}));
ASSERT_EQ(pool.at(0u), entt::entity{42});
ASSERT_EQ(pool.at(1u), static_cast<entt::entity>(entt::null));
// basice exception safety: no-leak guarantee, stored data contain valid values which may differ from the original values
ASSERT_EQ(pool.get(entt::entity{42}), 1);
}
TEST(Storage, ThrowingAllocator) {
entt::basic_storage<entt::entity, int, test::throwing_allocator<int>> pool;
test::throwing_allocator<int>::trigger_on_allocate = true;
// strong exception safety
ASSERT_THROW(pool.reserve(1u), test::throwing_allocator<int>::exception_type);
ASSERT_EQ(pool.capacity(), 0u);
test::throwing_allocator<int>::trigger_after_allocate = true;
// strong exception safety
ASSERT_THROW(pool.reserve(2 * ENTT_PACKED_PAGE), test::throwing_allocator<int>::exception_type);
ASSERT_EQ(pool.capacity(), 0u);
test::throwing_allocator<int>::trigger_on_pointer_copy = true;
// strong exception safety
ASSERT_THROW(pool.reserve(1u), test::throwing_allocator<int>::exception_type);
ASSERT_EQ(pool.capacity(), 0u);
pool.reserve(2 * ENTT_PACKED_PAGE);
test::throwing_allocator<test::throwing_allocator<int>::pointer>::trigger_on_pointer_copy = true;
// strong exception safety
ASSERT_THROW(pool.shrink_to_fit(), test::throwing_allocator<test::throwing_allocator<int>::pointer>::exception_type);
ASSERT_EQ(pool.capacity(), 2 * ENTT_PACKED_PAGE);
pool.shrink_to_fit();
test::throwing_allocator<int>::trigger_on_allocate = true;
// strong exception safety
ASSERT_THROW(pool.emplace(entt::entity{0}, 0), test::throwing_allocator<int>::exception_type);
ASSERT_FALSE(pool.contains(entt::entity{0}));
ASSERT_TRUE(pool.empty());
}

View File

@@ -0,0 +1,170 @@
#ifndef ENTT_ENTITY_THROWING_ALLOCATOR_HPP
#define ENTT_ENTITY_THROWING_ALLOCATOR_HPP
#include <cstddef>
#include <iterator>
#include <memory>
#include <type_traits>
namespace test {
template<typename Type>
class throwing_allocator {
template<typename Other>
friend class throwing_allocator;
struct fancy_pointer final {
using difference_type = typename std::iterator_traits<Type *>::difference_type;
using element_type = Type;
using value_type = element_type;
using pointer = value_type *;
using reference = value_type &;
using iterator_category = std::random_access_iterator_tag;
fancy_pointer(Type *init = nullptr)
: ptr{init}
{}
fancy_pointer(const fancy_pointer &other)
: ptr{other.ptr}
{
if(throwing_allocator::trigger_on_pointer_copy) {
throwing_allocator::trigger_on_pointer_copy = false;
throw test_exception{};
}
}
fancy_pointer & operator++() {
return ++ptr, *this;
}
fancy_pointer operator++(int) {
auto orig = *this;
return ++(*this), orig;
}
fancy_pointer & operator--() {
return --ptr, *this;
}
fancy_pointer operator--(int) {
auto orig = *this;
return operator--(), orig;
}
fancy_pointer & operator+=(const difference_type value) {
return (ptr += value, *this);
}
fancy_pointer operator+(const difference_type value) const {
auto copy = *this;
return (copy += value);
}
fancy_pointer & operator-=(const difference_type value) {
return (ptr -= value, *this);
}
fancy_pointer operator-(const difference_type value) const {
auto copy = *this;
return (copy -= value);
}
difference_type operator-(const fancy_pointer &other) const {
return ptr - other.ptr;
}
[[nodiscard]] reference operator[](const difference_type value) const {
return ptr[value];
}
[[nodiscard]] bool operator==(const fancy_pointer &other) const {
return other.ptr == ptr;
}
[[nodiscard]] bool operator!=(const fancy_pointer &other) const {
return !(*this == other);
}
[[nodiscard]] bool operator<(const fancy_pointer &other) const {
return ptr > other.ptr;
}
[[nodiscard]] bool operator>(const fancy_pointer &other) const {
return ptr < other.ptr;
}
[[nodiscard]] bool operator<=(const fancy_pointer &other) const {
return !(*this > other);
}
[[nodiscard]] bool operator>=(const fancy_pointer &other) const {
return !(*this < other);
}
explicit operator bool() const {
return (ptr != nullptr);
}
[[nodiscard]] pointer operator->() const {
return ptr;
}
[[nodiscard]] reference operator*() const {
return *ptr;
}
private:
Type *ptr;
};
struct test_exception {};
public:
using value_type = Type;
using pointer = fancy_pointer;
using const_pointer = fancy_pointer;
using void_pointer = fancy_pointer;
using const_void_pointer = fancy_pointer;
using propagate_on_container_move_assignment = std::true_type;
using exception_type = test_exception;
constexpr throwing_allocator() = default;
template<class Other>
throwing_allocator(const throwing_allocator<Other> &other)
: allocator{other.allocator}
{}
pointer allocate(std::size_t length) {
if(trigger_on_allocate) {
trigger_on_allocate = false;
throw test_exception{};
}
trigger_on_allocate = trigger_after_allocate;
trigger_after_allocate = false;
return allocator.allocate(length);
}
void deallocate(pointer mem, std::size_t length) {
allocator.deallocate(mem.operator->(), length);
}
static inline bool trigger_on_allocate{};
static inline bool trigger_after_allocate{};
static inline bool trigger_on_pointer_copy{};
private:
std::allocator<Type> allocator;
};
}
#endif

View File

@@ -0,0 +1,49 @@
#ifndef ENTT_ENTITY_THROWING_COMPONENT_HPP
#define ENTT_ENTITY_THROWING_COMPONENT_HPP
namespace test {
class throwing_component {
struct test_exception {};
public:
using exception_type = test_exception;
throwing_component(int value)
: data{value}
{}
throwing_component(const throwing_component &other)
: data{other.data}
{
if(data == trigger_on_value) {
throw exception_type{};
}
}
throwing_component & operator=(const throwing_component &other) {
if(other.data == trigger_on_value) {
throw exception_type{};
}
data = other.data;
return *this;
}
operator int() const {
return data;
}
static inline int trigger_on_value{};
private:
int data{};
};
}
#endif

View File

@@ -0,0 +1,52 @@
#ifndef ENTT_ENTITY_THROWING_ENTITY_HPP
#define ENTT_ENTITY_THROWING_ENTITY_HPP
namespace test {
class throwing_entity {
struct test_exception {};
public:
using entity_type = std::uint32_t;
using exception_type = test_exception;
static constexpr entity_type null = entt::null;
throwing_entity(entity_type value)
: entt{value}
{}
throwing_entity(const throwing_entity &other)
: entt{other.entt}
{
if(entt == trigger_on_entity) {
throw exception_type{};
}
}
throwing_entity & operator=(const throwing_entity &other) {
if(other.entt == trigger_on_entity) {
throw exception_type{};
}
entt = other.entt;
return *this;
}
operator entity_type() const {
return entt;
}
static inline entity_type trigger_on_entity{null};
private:
entity_type entt{};
};
}
#endif