From 99480f547cb377238e49ec4b9a2ff4e723ae8545 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Tue, 9 Jul 2024 22:40:29 -0400 Subject: [PATCH] Allocator pmr conformance, tests, comments, style. --- include/bitcoin/system/allocator.hpp | 169 ++++---- include/bitcoin/system/arena.hpp | 23 +- include/bitcoin/system/exceptions.hpp | 3 +- include/bitcoin/system/warnings.hpp | 3 + src/arena.cpp | 5 + test/allocator.cpp | 543 +++++++++++++++++++++++++- test/arena.cpp | 84 +--- test/test.hpp | 37 +- 8 files changed, 715 insertions(+), 152 deletions(-) diff --git a/include/bitcoin/system/allocator.hpp b/include/bitcoin/system/allocator.hpp index 36cf505153..c60987cc86 100644 --- a/include/bitcoin/system/allocator.hpp +++ b/include/bitcoin/system/allocator.hpp @@ -31,15 +31,20 @@ namespace libbitcoin { /// No-default-fill polymorphic allocator. +/// Strictly conforms to std::pmr::polymorphic_allocator. +/// Does not default to std::pmr::get_default_resource() but +/// default_arena::get() exposes the same underlying global default allocators. template class allocator { public: template friend class allocator; - using value_type = Value; + /// construct/assign + /// ----------------------------------------------------------------------- + template allocator(const allocator& other) NOEXCEPT : allocator{ other.arena_ } @@ -51,27 +56,20 @@ class allocator { } - allocator(arena* const value) NOEXCEPT + allocator(arena* value) NOEXCEPT : arena_{ value } { } + allocator(const allocator&) = default; allocator& operator=(const allocator&) = delete; - NODISCARD ALLOCATOR Value* allocate(size_t count) - { - const auto bytes = get_byte_size(count); - return static_cast(arena_->allocate(bytes, alignof(Value))); - } - - void deallocate(Value* ptr, size_t count) NOEXCEPT - { - // No need to verify multiplication overflow. - arena_->deallocate(ptr, count * sizeof(Value), alignof(Value)); - } + /// allocate_bytes/deallocate_bytes + /// ----------------------------------------------------------------------- + /// These allocate/deallocate bytes, without consideration of other types. NODISCARD ALLOCATOR void* allocate_bytes(size_t bytes, - size_t align=alignof(max_align_t)) + size_t align=alignof(max_align_t)) THROWS { return arena_->allocate(bytes, align); } @@ -82,8 +80,12 @@ class allocator arena_->deallocate(ptr, bytes, align); } + /// allocate_object/deallocate_object + /// ----------------------------------------------------------------------- + /// These allocate/deallocate bytes of Type size, for count of Type. + template - NODISCARD ALLOCATOR Type* allocate_object(size_t count=1) + NODISCARD ALLOCATOR Type* allocate_object(size_t count=1) THROWS { const auto bytes = get_byte_size(count); return static_cast(allocate_bytes(bytes, alignof(Type))); @@ -95,20 +97,31 @@ class allocator deallocate_bytes(ptr, count * sizeof(Type), alignof(Type)); } - ////template - ////NODISCARD ALLOCATOR Type* new_object() - ////{ - //// // Default construction fill is bypassed here. - //// return allocate_object(); - ////} + /// allocate/deallocate + /// ----------------------------------------------------------------------- + /// These allocate/deallocate bytes of Value size, for count of Value. + + NODISCARD ALLOCATOR Value* allocate(size_t count) THROWS + { + return allocate_object(count); + } + + void deallocate(Value* ptr, size_t count) NOEXCEPT + { + return deallocate_object(ptr, count); + } + + /// new_object/delete_object + /// ----------------------------------------------------------------------- + /// These allocate & construct / destruct & deallocate. - template - NODISCARD ALLOCATOR Type* new_object(Args&&... args) + template + NODISCARD ALLOCATOR Type* new_object(Args&&... args) THROWS { // construct_guard ensures deallocation if construct exception. auto ptr = allocate_object(); construct_guard guard{ arena_, ptr }; - construct(ptr, std::forward(args)...); + construct(ptr, std::forward(args)...); guard.arena_ = nullptr; return ptr; } @@ -116,33 +129,73 @@ class allocator template void delete_object(Type* ptr) NOEXCEPT { - destroy_in_place(*ptr); + destroy(ptr); deallocate_object(ptr); } - ////template - ////void construct(Type*) NOEXCEPT - ////{ - //// // Default construction fill is bypassed here. - ////} + /// construct/destroy + /// ----------------------------------------------------------------------- + /// These neither allocate nor deallocate. - template - void construct(Type* ptr, Args&&... arguments) + // Clang is not yet C++20 compliant in terms of aggregate initialization. + // See [reviews.llvm.org/D140327] for details, resolved in future releases. + template + void construct(Type* ptr, Args&&... arguments) THROWS { + BC_PUSH_WARNING(NO_IMPLICIT_CONVERTABLE_CAST) + auto at = const_cast(static_cast(ptr)); + BC_POP_WARNING() + std::apply ( - [ptr](auto&&... args) + // std::apply forwards second argument (tuple) as args to lambda. + [at](auto&&... args) { - return ::new( - const_cast(static_cast(ptr))) - Type(std::forward(args)...); + BC_PUSH_WARNING(NO_ARRAY_TO_POINTER_DECAY) + BC_PUSH_WARNING(NO_RETURN_MOVEABLE_HEAP_OBJECT) + + // Construct Type(...) in previously-allocated address 'at'. + return ::new(at) Type(std::forward(args)...); + + BC_POP_WARNING() + BC_POP_WARNING() }, + + // std::uses_allocator_construction_args merges *this as last arg + // if exists Type(..., const Alloc& alloc), otherwise forwards args. std::uses_allocator_construction_args(*this, std::forward(arguments)...) ); } - // Container copy results in default arena!!! + // www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2875r0.pdf + // To become undeprecated in C++26 (which basically means it is not now). + template + void destroy(Type* ptr) NOEXCEPT + { + if constexpr (std::is_array_v) + { + using element = std::iter_value_t; + if constexpr (!std::is_trivially_destructible_v) + { + const auto last = *ptr + std::extent_v; + for (auto first = *ptr; first != last; ++first) + { + // Recurse until non-array or trivially destructible. + destroy(first); + } + } + } + else + { + ptr->~Type(); + } + } + + /// other + /// ----------------------------------------------------------------------- + + // polymorphic allocators do not propagate on container copy construction! allocator select_on_container_copy_construction() const NOEXCEPT { return {}; @@ -153,14 +206,6 @@ class allocator return arena_; } - // To become undeprecated in C++26. - // www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2875r0.pdf - template - DEPRECATED void destroy(Type* ptr) NOEXCEPT - { - destroy_in_place(*ptr); - } - friend bool operator==(const allocator& left, const allocator& right) NOEXCEPT { @@ -190,40 +235,24 @@ class allocator if constexpr (Size > 1u) { if (count > (std::numeric_limits::max() / Size)) - throw overflow_exception("allocation overflow"); + throw bad_array_new_length(); } return count * Size; } - template - constexpr void destroy_in_place(Type& object_) NOEXCEPT - { - if constexpr (std::is_array_v) - { - destroy_range(object_, object_ + std::extent_v); - } - else - { - object_.~Type(); - } - } - - template - constexpr void destroy_range(First first, const Last last) NOEXCEPT - { - using element = std::iter_value_t; - - // Optimization for debug mode, in release mode this is removed. - if constexpr (!std::is_trivially_destructible_v) - for (; first != last; ++first) - destroy_in_place(*first); - } - private: arena* arena_; }; +/// Same as friend equality but allows conversion to allocator. +template +inline bool operator==(const allocator& left, + const allocator& right) NOEXCEPT +{ + return left == right; +} + } // namespace libbitcoin #endif diff --git a/include/bitcoin/system/arena.hpp b/include/bitcoin/system/arena.hpp index b588a73777..f784579af9 100644 --- a/include/bitcoin/system/arena.hpp +++ b/include/bitcoin/system/arena.hpp @@ -23,35 +23,29 @@ namespace libbitcoin { -/// Memory arena interface, for use with our (polymorphic) allocator. +/// Memory resource interface, for use with our (polymorphic) allocator. +/// Strictly conforms to std::pmr::memory_resource. class arena { public: - static constexpr auto max_align = alignof(max_align_t); virtual ~arena() NOEXCEPT = default; /// Allocate bytes with alignment (align must be power of 2). + /// Throws if the requested size and alignment cannot be obtained. NODISCARD ALLOCATOR void* allocate(size_t bytes, - size_t align=max_align) THROWS + size_t align=alignof(max_align_t)) THROWS { - // actual allocation. - auto ptr = do_allocate(bytes, align); - - // non-allocating placement. - // "The standard library implementation performs no action and returns - // ptr unmodified. The behavior is undefined if this function is called - // through a placement new expression and ptr is a null pointer." - return ::operator new(bytes, ptr); + return do_allocate(bytes, align); } /// Deallocate allocated bytes with alignment (align must be power of 2). void deallocate(void* ptr, const size_t bytes, - size_t align=max_align) NOEXCEPT + size_t align=alignof(max_align_t)) NOEXCEPT { return do_deallocate(ptr, bytes, align); } - /// Other can deallocate memory allocated by this. + /// Other can deallocate memory allocated by this and vice versa. NODISCARD bool is_equal(const arena& other) const NOEXCEPT { return do_is_equal(other); @@ -63,6 +57,9 @@ class arena virtual bool do_is_equal(const arena& other) const NOEXCEPT = 0; }; +/// Left can deallocate memory allocated by right and vice versa. +bool operator==(const arena& left, const arena& right) NOEXCEPT; + /// *************************************************************************** /// BE AWARE of the risks of memory relocation. Generally speaking a custom /// resource [arena] must be used in strict isolation (avoiding relocation). diff --git a/include/bitcoin/system/exceptions.hpp b/include/bitcoin/system/exceptions.hpp index b76fe29255..595fde07b2 100644 --- a/include/bitcoin/system/exceptions.hpp +++ b/include/bitcoin/system/exceptions.hpp @@ -67,7 +67,8 @@ using istream_exception = boost::program_options::invalid_option_value; using ifstream_exception = boost::program_options::reading_file; /// Allocation. -using allocation_exception = std::bad_alloc; +////using allocation_exception = std::bad_alloc; +using bad_array_new_length = std::bad_array_new_length; } // namespace libbitcoin diff --git a/include/bitcoin/system/warnings.hpp b/include/bitcoin/system/warnings.hpp index f779033a48..c964266589 100644 --- a/include/bitcoin/system/warnings.hpp +++ b/include/bitcoin/system/warnings.hpp @@ -57,6 +57,8 @@ #define NO_READ_OVERRUN 6385 #define NO_WRITE_OVERRUN 6386 #define NO_DELETE_RAW_POINTER 26401 + #define NO_RETURN_MOVEABLE_HEAP_OBJECT 26402 + #define NO_UNCLEARED_OWNER_POINTER 26403 #define NO_MALLOC_OR_FREE 26408 #define NO_NEW_OR_DELETE 26409 #define NO_UNUSED_LOCAL_SMART_PTR 26414 @@ -74,6 +76,7 @@ #define NO_STATIC_CAST 26467 #define NO_CASTS_FOR_ARITHMETIC_CONVERSION 26472 #define NO_IDENTITY_CAST 26473 + #define NO_IMPLICIT_CONVERTABLE_CAST 26474 #define NO_POINTER_ARITHMETIC 26481 #define NO_DYNAMIC_ARRAY_INDEXING 26482 #define NO_ARRAY_TO_POINTER_DECAY 26485 diff --git a/src/arena.cpp b/src/arena.cpp index dda6947538..b922f73b4a 100644 --- a/src/arena.cpp +++ b/src/arena.cpp @@ -22,6 +22,11 @@ namespace libbitcoin { BC_PUSH_WARNING(NO_NEW_OR_DELETE) +bool operator==(const arena& left, const arena& right) NOEXCEPT +{ + return &left == &right || left.is_equal(right); +} + void* default_arena::do_allocate(size_t bytes, size_t) THROWS { ////if (align > __STDCPP_DEFAULT_NEW_ALIGNMENT__) diff --git a/test/allocator.cpp b/test/allocator.cpp index 5249eab828..4801f4675c 100644 --- a/test/allocator.cpp +++ b/test/allocator.cpp @@ -20,9 +20,548 @@ BOOST_AUTO_TEST_SUITE(allocator_tests) -BOOST_AUTO_TEST_CASE(allocator_test) +// Doing some low level stuff we should'nt normally do. +BC_PUSH_WARNING(NO_NEW_OR_DELETE) +BC_PUSH_WARNING(NO_UNGUARDED_POINTERS) +BC_PUSH_WARNING(NO_DELETE_RAW_POINTER) +BC_PUSH_WARNING(NO_UNCLEARED_OWNER_POINTER) + +// Clang is not yet C++20 compliant in terms of aggregate initialization. +// See [reviews.llvm.org/D140327] for details. So vectors provide constructors. +/////////////////////////////////////////////////////////////////////////////// + +struct simple +{ + simple(size_t arg={}) NOEXCEPT + : value(arg) + { + } + + size_t value{}; +}; +static_assert(std::is_constructible_v); +static_assert(std::is_constructible_v); + +struct aggregate +{ + aggregate(size_t arg1={}, size_t arg2={}) NOEXCEPT + : value1(arg1), value2(arg2) + { + } + + size_t value1{}; + size_t value2{}; +}; +static_assert(std::is_constructible_v); +static_assert(std::is_constructible_v); +static_assert(std::is_constructible_v); + +struct compound +{ + static constexpr data_array<3> null_array{ 0x00, 0x00, 0x00 }; + compound(size_t arg1={}, const data_array<3>& arg2={}) NOEXCEPT + : value(arg1), bytes(arg2) + { + } + + size_t value{}; + data_array<3> bytes{}; +}; +static_assert(std::is_constructible_v); +static_assert(std::is_constructible_v); +static_assert(std::is_constructible_v>); + +struct hierarchy +{ + hierarchy(const compound& arg1={}, const data_chunk& arg2={}) NOEXCEPT + : contained(arg1), chunk(arg2) + { + } + + compound contained{}; + data_chunk chunk{}; +}; +static_assert(std::is_constructible_v); +static_assert(std::is_constructible_v); +static_assert(std::is_constructible_v); + +/////////////////////////////////////////////////////////////////////////////// + +// constructor + +BOOST_AUTO_TEST_CASE(allocator__allocator1__default__default_arena) +{ + const allocator instance1{}; + BOOST_REQUIRE_EQUAL(instance1.resource(), default_arena::get()); +} + +BOOST_AUTO_TEST_CASE(allocator__allocator1__resources__same_instance) +{ + const allocator instance1{}; + const allocator instance2{}; + BOOST_REQUIRE_EQUAL(instance1.resource(), instance2.resource()); +} + +BOOST_AUTO_TEST_CASE(allocator__allocator2__always__expected_resource) +{ + default_arena arena1{}; + const allocator instance1{ &arena1 }; + BOOST_REQUIRE_EQUAL(instance1.resource(), &arena1); +} + +// Templated copy construction. +BOOST_AUTO_TEST_CASE(allocator__allocator3__always__expected_resource) +{ + default_arena arena1{}; + const allocator> instance1{ &arena1 }; + const allocator> instance2{ instance1 }; + BOOST_REQUIRE_EQUAL(instance2.resource(), &arena1); +} + +// Default copy construction. +BOOST_AUTO_TEST_CASE(allocator__allocator4__always__expected_resource) +{ + default_arena arena1{}; + const allocator instance1{ &arena1 }; + const allocator instance2{ instance1 }; + BOOST_REQUIRE_EQUAL(instance2.resource(), &arena1); +} + +// allocate +// test::mock_arena::allocate always returns null. + +BOOST_AUTO_TEST_CASE(allocator__allocate__overflow__throws_bad_array_new_length) +{ + test::mock_arena arena{}; + allocator instance1{ &arena }; + + // max_size_t * sizeof(int) overflows. + BOOST_REQUIRE_THROW(is_null(instance1.allocate(max_size_t)), bad_array_new_length); +} + +BOOST_AUTO_TEST_CASE(allocator__allocate__max_size__do_allocate_expected_values) +{ + test::mock_arena arena{}; + allocator instance1{ &arena }; + constexpr auto count = max_size_t; + BOOST_REQUIRE_EQUAL((void*)instance1.allocate(count), &arena); + BOOST_REQUIRE_EQUAL(arena.do_allocate_bytes, count * sizeof(uint8_t)); + BOOST_REQUIRE_EQUAL(arena.do_allocate_align, alignof(uint8_t)); +} + +BOOST_AUTO_TEST_CASE(allocator__allocate__values__do_allocate_expected_values) +{ + test::mock_arena arena{}; + allocator instance1{ &arena }; + constexpr auto count = 42_size; + BOOST_REQUIRE_EQUAL((void*)instance1.allocate(count), &arena); + BOOST_REQUIRE_EQUAL(arena.do_allocate_bytes, count * sizeof(double)); + BOOST_REQUIRE_EQUAL(arena.do_allocate_align, alignof(double)); +} + +// deallocate + +BOOST_AUTO_TEST_CASE(allocator__deallocate__overflow__does_not_throw) +{ + test::mock_arena arena{}; + allocator instance1{ &arena }; + int memory{}; + + // max_size_t * sizeof(int) overflows. + BOOST_REQUIRE_NO_THROW(instance1.deallocate(&memory, max_size_t)); + BOOST_REQUIRE_EQUAL(arena.do_deallocate_ptr, &memory); + BOOST_REQUIRE_EQUAL(arena.do_deallocate_bytes, max_size_t * sizeof(int)); + BOOST_REQUIRE_EQUAL(arena.do_deallocate_align, alignof(int)); +} + +BOOST_AUTO_TEST_CASE(allocator__deallocate__values__do_deallocate_expected_values) +{ + test::mock_arena arena{}; + allocator instance1{ &arena }; + constexpr auto count = 42_size; + double memory{}; + instance1.deallocate(&memory, count); + BOOST_REQUIRE_EQUAL(arena.do_deallocate_ptr, &memory); + BOOST_REQUIRE_EQUAL(arena.do_deallocate_bytes, count * sizeof(double)); + BOOST_REQUIRE_EQUAL(arena.do_deallocate_align, alignof(double)); +} + +// allocate_bytes +// test::mock_arena::allocate returns self if do_allocate is invoked. + +BOOST_AUTO_TEST_CASE(allocator__allocate_bytes1__values__do_allocate_expected_values) +{ + test::mock_arena arena{}; + allocator instance1{ &arena }; + constexpr auto bytes = 42_size; + BOOST_REQUIRE_EQUAL((void*)instance1.allocate_bytes(bytes), &arena); + BOOST_REQUIRE_EQUAL(arena.do_allocate_bytes, bytes); + BOOST_REQUIRE_EQUAL(arena.do_allocate_align, alignof(max_align_t)); +} + +BOOST_AUTO_TEST_CASE(allocator__allocate_bytes2__values__do_allocate_expected_values) +{ + test::mock_arena arena{}; + allocator instance1{ &arena }; + constexpr auto bytes = 42_size; + constexpr auto align = 32_size; + BOOST_REQUIRE_EQUAL((void*)instance1.allocate_bytes(bytes, align), &arena); + BOOST_REQUIRE_EQUAL(arena.do_allocate_bytes, bytes); + BOOST_REQUIRE_EQUAL(arena.do_allocate_align, align); +} + +// deallocate_bytes + +BOOST_AUTO_TEST_CASE(allocator__deallocate_bytes__values__do_deallocate_expected_values) +{ + test::mock_arena arena{}; + allocator instance1{ &arena }; + constexpr auto bytes = 42_size; + double memory{}; + instance1.deallocate_bytes(&memory, bytes); + BOOST_REQUIRE_EQUAL(arena.do_deallocate_ptr, &memory); + BOOST_REQUIRE_EQUAL(arena.do_deallocate_bytes, bytes); + BOOST_REQUIRE_EQUAL(arena.do_deallocate_align, alignof(max_align_t)); +} + +BOOST_AUTO_TEST_CASE(allocator__deallocate_bytes2__values__do_deallocate_expected_values) +{ + test::mock_arena arena{}; + allocator instance1{ &arena }; + constexpr auto bytes = 42_size; + constexpr auto align = 32_size; + double memory{}; + instance1.deallocate_bytes(&memory, bytes, align); + BOOST_REQUIRE_EQUAL(arena.do_deallocate_ptr, &memory); + BOOST_REQUIRE_EQUAL(arena.do_deallocate_bytes, bytes); + BOOST_REQUIRE_EQUAL(arena.do_deallocate_align, align); +} + +// allocate_object + +BOOST_AUTO_TEST_CASE(allocator__allocate_object__overflow__throws_bad_array_new_length) { - BOOST_REQUIRE(true); + test::mock_arena arena{}; + allocator instance1{ &arena }; + + // max_size_t * sizeof(int) overflows. + BOOST_REQUIRE_THROW(is_null(instance1.allocate_object(max_size_t)), bad_array_new_length); } +BOOST_AUTO_TEST_CASE(allocator__allocate_object1__values__do_allocate_expected_values) +{ + test::mock_arena arena{}; + allocator instance1{ &arena }; + constexpr auto size = 42u; + using other_type = data_array; + BOOST_REQUIRE_EQUAL((void*)instance1.allocate_object(), &arena); + BOOST_REQUIRE_EQUAL(arena.do_allocate_align, alignof(other_type)); + BOOST_REQUIRE_EQUAL(arena.do_allocate_bytes, size); +} + +BOOST_AUTO_TEST_CASE(allocator__allocate_object2__values__do_allocate_expected_values) +{ + test::mock_arena arena{}; + allocator instance1{ &arena }; + constexpr auto count = 2u; + constexpr auto size = 42u; + using other_type = data_array; + BOOST_REQUIRE_EQUAL((void*)instance1.allocate_object(count), &arena); + BOOST_REQUIRE_EQUAL(arena.do_allocate_align, alignof(other_type)); + BOOST_REQUIRE_EQUAL(arena.do_allocate_bytes, count * size); +} + +// deallocate_object + +BOOST_AUTO_TEST_CASE(allocator__deallocate_object__overflow__does_not_throw) +{ + test::mock_arena arena{}; + allocator instance1{ &arena }; + int memory{}; + + // max_size_t * sizeof(int) overflows. + BOOST_REQUIRE_NO_THROW(instance1.deallocate_object(&memory, max_size_t)); + BOOST_REQUIRE_EQUAL(arena.do_deallocate_ptr, &memory); + BOOST_REQUIRE_EQUAL(arena.do_deallocate_bytes, max_size_t * sizeof(int)); + BOOST_REQUIRE_EQUAL(arena.do_deallocate_align, alignof(int)); +} + +BOOST_AUTO_TEST_CASE(allocator__deallocate_object1__values__do_deallocate_expected_values) +{ + test::mock_arena arena{}; + allocator instance1{ &arena }; + double memory{}; + instance1.deallocate_object(&memory); + BOOST_REQUIRE_EQUAL(arena.do_deallocate_ptr, &memory); + BOOST_REQUIRE_EQUAL(arena.do_deallocate_bytes, sizeof(double)); + BOOST_REQUIRE_EQUAL(arena.do_deallocate_align, alignof(double)); +} + +BOOST_AUTO_TEST_CASE(allocator__deallocate_object2__values__do_deallocate_expected_values) +{ + test::mock_arena arena{}; + allocator instance1{ &arena }; + constexpr auto count = 42_size; + double memory{}; + instance1.deallocate_object(&memory, count); + BOOST_REQUIRE_EQUAL(arena.do_deallocate_ptr, &memory); + BOOST_REQUIRE_EQUAL(arena.do_deallocate_bytes, count * sizeof(double)); + BOOST_REQUIRE_EQUAL(arena.do_deallocate_align, alignof(double)); +} + +// new_object + +BOOST_AUTO_TEST_CASE(allocator__new_object__contruct_throws__calls_deallocate_and_propagates_exception) +{ + struct thrower + { + thrower(const char* text) THROWS + { + if (!is_null(text)) + throw overflow_exception(text); + } + }; + + test::mock_arena arena{}; + allocator instance1{ &arena }; + + // max_size_t * sizeof(int) overflows. + BOOST_REQUIRE_THROW(is_null(instance1.new_object("overflow")), overflow_exception); + + // Allocated and deallocated despite construction throw. + BOOST_REQUIRE_EQUAL(arena.do_allocate_bytes, sizeof(thrower)); + BOOST_REQUIRE_EQUAL(arena.do_allocate_align, alignof(thrower)); + + // Deallocate is invoked by the construct_guard. + BOOST_REQUIRE_EQUAL(arena.do_deallocate_ptr, &arena); + BOOST_REQUIRE_EQUAL(arena.do_deallocate_bytes, sizeof(thrower)); + BOOST_REQUIRE_EQUAL(arena.do_deallocate_align, alignof(thrower)); +} + +// select_on_container_copy_construction + +BOOST_AUTO_TEST_CASE(allocator__select_on_container_copy_construction__always__contructs_default) +{ + test::mock_arena arena{}; + const allocator instance1{ &arena }; + const auto instance2 = instance1.select_on_container_copy_construction(); + BOOST_REQUIRE_EQUAL(instance1.resource(), &arena); + BOOST_REQUIRE_EQUAL(instance2.resource(), default_arena::get()); + BOOST_REQUIRE_NE(instance1.resource(), default_arena::get()); + BOOST_REQUIRE_NE(instance2.resource(), &arena); +} + +// resource + +BOOST_AUTO_TEST_CASE(allocator__resource__default__default) +{ + BOOST_REQUIRE_EQUAL(allocator{}.resource(), default_arena::get()); +} + +BOOST_AUTO_TEST_CASE(allocator__resource__non_default__expected) +{ + test::mock_arena arena{}; + BOOST_REQUIRE_EQUAL(allocator{ &arena }.resource(), &arena); +} + +// equality + +BOOST_AUTO_TEST_CASE(allocator__equality__different_arena__false) +{ + default_arena arena1{}; + test::reporting_arena arena2{}; + const allocator instance1{ &arena1 }; + const allocator instance2{ &arena2 }; + BOOST_REQUIRE(!(instance1 == instance2)); +} + +BOOST_AUTO_TEST_CASE(allocator__equality__same_arena__true) +{ + default_arena arena{}; + const allocator instance1{ &arena }; + const allocator instance2{ &arena }; + BOOST_REQUIRE(instance1 == instance2); +} + +// new_object/construct +// new_object(args) and construct(ptr, args) are not mocked. + +BOOST_AUTO_TEST_CASE(allocator__new_object__integral_type__expected) +{ + test::reporting_arena arena{}; + allocator instance1{ &arena }; + constexpr auto expected_value = 42; + const auto ptr = instance1.new_object(expected_value); + BOOST_REQUIRE(!is_null(ptr)); + BOOST_REQUIRE_EQUAL(*ptr, expected_value); + BOOST_REQUIRE_EQUAL(arena.inc_count, one); + BOOST_REQUIRE_EQUAL(arena.dec_count, zero); + BOOST_REQUIRE_EQUAL(arena.inc_bytes, sizeof(int)); + ::operator delete(ptr); +} + +BOOST_AUTO_TEST_CASE(allocator__new_object__simple_struct__expected) +{ + + test::reporting_arena arena{}; + allocator instance1{ &arena }; + const auto ptr = instance1.new_object(42); + BOOST_REQUIRE(!is_null(ptr)); + BOOST_REQUIRE_EQUAL(ptr->value, 42u); + BOOST_REQUIRE_EQUAL(arena.inc_count, one); + BOOST_REQUIRE_EQUAL(arena.dec_count, zero); + BOOST_REQUIRE_EQUAL(arena.inc_bytes, sizeof(size_t)); + ::operator delete(ptr); +} + +BOOST_AUTO_TEST_CASE(allocator__new_object__aggregate_struct__expected) +{ + test::reporting_arena arena{}; + allocator instance1{ &arena }; + const auto ptr = instance1.new_object(42, 24); + BOOST_REQUIRE(!is_null(ptr)); + BOOST_REQUIRE_EQUAL(ptr->value1, 42u); + BOOST_REQUIRE_EQUAL(ptr->value2, 24u); + BOOST_REQUIRE_EQUAL(arena.inc_count, one); + BOOST_REQUIRE_EQUAL(arena.dec_count, zero); + BOOST_REQUIRE_EQUAL(arena.inc_bytes, 2 * sizeof(size_t)); + ::operator delete(ptr); +} + +BOOST_AUTO_TEST_CASE(allocator__new_object__compound_struct__expected) +{ + test::reporting_arena arena{}; + allocator instance1{ &arena }; + const auto ptr = instance1.new_object(); + BOOST_REQUIRE(!is_null(ptr)); + BOOST_REQUIRE_EQUAL(ptr->value, 0u); + BOOST_REQUIRE_EQUAL(ptr->bytes, compound::null_array); + BOOST_REQUIRE_EQUAL(arena.inc_count, one); + BOOST_REQUIRE_EQUAL(arena.dec_count, zero); + ::operator delete(ptr); +} + +BOOST_AUTO_TEST_CASE(allocator__new_object__hierarchy_struct__expected) +{ + test::reporting_arena arena{}; + allocator instance1{ &arena }; + const auto ptr = instance1.new_object(); + BOOST_REQUIRE(!is_null(ptr)); + BOOST_REQUIRE_EQUAL(ptr->contained.value, 0u); + BOOST_REQUIRE_EQUAL(ptr->contained.bytes, compound::null_array); + BOOST_REQUIRE(ptr->chunk.empty()); + BOOST_REQUIRE_EQUAL(arena.inc_count, one); + BOOST_REQUIRE_EQUAL(arena.dec_count, zero); + + // data_chunk (vector) element allocations are independent of allocator + // because the allocator doesn't propagate into the struct elements, + // because the struct does not satisfy std::uses_allocator_v. + const auto before = arena.inc_bytes; + ptr->chunk.push_back({}); + BOOST_REQUIRE_EQUAL(arena.inc_bytes, before); + BOOST_REQUIRE_EQUAL(arena.inc_count, one); + + ::operator delete(ptr); +} + +// delete_object/destroy +// delete_object(ptr) and destroy(ptr) are not mocked. + +BOOST_AUTO_TEST_CASE(allocator__delete_object__integral_type__expected) +{ + const auto ptr = ::new int{ 42 }; + BOOST_REQUIRE(!is_null(ptr)); + + test::reporting_arena arena{}; + allocator instance1{ &arena }; + instance1.delete_object(ptr); + BOOST_REQUIRE_EQUAL(arena.inc_count, zero); + BOOST_REQUIRE_EQUAL(arena.dec_count, one); + BOOST_REQUIRE_EQUAL(arena.dec_bytes, sizeof(int)); +} + +BOOST_AUTO_TEST_CASE(allocator__delete_object__simple_struct__expected) +{ + const auto ptr = ::new simple{ 42 }; + BOOST_REQUIRE(!is_null(ptr)); + + test::reporting_arena arena{}; + allocator instance1{ &arena }; + instance1.delete_object(ptr); + BOOST_REQUIRE_EQUAL(arena.inc_count, zero); + BOOST_REQUIRE_EQUAL(arena.dec_count, one); + BOOST_REQUIRE_EQUAL(arena.dec_bytes, sizeof(size_t)); +} + +BOOST_AUTO_TEST_CASE(allocator__delete_object__aggregate_struct__expected) +{ + const auto ptr = ::new aggregate{ 42, 24 }; + BOOST_REQUIRE(!is_null(ptr)); + + test::reporting_arena arena{}; + allocator instance1{ &arena }; + instance1.delete_object(ptr); + BOOST_REQUIRE_EQUAL(arena.inc_count, zero); + BOOST_REQUIRE_EQUAL(arena.dec_count, one); + BOOST_REQUIRE_EQUAL(arena.dec_bytes, 2 * sizeof(size_t)); +} + +BOOST_AUTO_TEST_CASE(allocator__delete_object__compound_struct__expected) +{ + const auto ptr = ::new compound{ 42, { 0x01, 0x02, 0x03 } }; + BOOST_REQUIRE(!is_null(ptr)); + + test::reporting_arena arena{}; + allocator instance1{ &arena }; + instance1.delete_object(ptr); + BOOST_REQUIRE_EQUAL(arena.inc_count, zero); + BOOST_REQUIRE_EQUAL(arena.dec_count, one); +} + +BOOST_AUTO_TEST_CASE(allocator__delete_object__hierarchy_struct__expected) +{ + // data_chunk (vector) element allocations are independent of allocator + // because the allocator doesn't propagate into the struct elements, + // because the struct does not satisfy std::uses_allocator_v. + const auto ptr = ::new hierarchy{ { 42, { 0x01, 0x02, 0x03 } }, { 0x24, 0x25 } }; + BOOST_REQUIRE(!is_null(ptr)); + + test::reporting_arena arena{}; + allocator instance1{ &arena }; + instance1.delete_object(ptr); + BOOST_REQUIRE_EQUAL(arena.inc_count, zero); + BOOST_REQUIRE_EQUAL(arena.dec_count, one); +} + +BC_POP_WARNING() +BC_POP_WARNING() +BC_POP_WARNING() +BC_POP_WARNING() + BOOST_AUTO_TEST_SUITE_END() + +// This is where the magic happens. +////template +////constexpr auto uses_allocator_construction_args(const Allocator& alloc, Types&&... args) noexcept +////{ +//// if constexpr (!std::uses_allocator_v, Allocator>) +//// { +//// static_assert(std::is_constructible_v, "If std::uses_allocator_v, Alloc> is false, T must be constructible from Types..."); +//// +//// (void)alloc; +//// return std::forward_as_tuple(std::forward(args)...); +//// } +//// else if constexpr (std::is_constructible_v) +//// { +//// using return_type = std::tuple; +//// return return_type{ std::allocator_arg, alloc, std::forward(args)... }; +//// } +//// else if constexpr (std::is_constructible_v) +//// { +//// return std::forward_as_tuple(std::forward(args)..., alloc); +//// } +//// else +//// { +//// static_assert(false, "T must be constructible from either (std::allocator_arg_t, const Alloc&, Types...) or (Types..., const Alloc&) if std::uses_allocator_v, Alloc> is true"); +//// } +////} diff --git a/test/arena.cpp b/test/arena.cpp index 1a03dafd42..1705031898 100644 --- a/test/arena.cpp +++ b/test/arena.cpp @@ -23,74 +23,41 @@ BOOST_AUTO_TEST_SUITE(arena_tests) BC_PUSH_WARNING(NO_NEW_OR_DELETE) BC_PUSH_WARNING(NO_DELETE_RAW_POINTER) -class test_arena - : public arena -{ -public: - size_t do_allocate_bytes{}; - size_t do_allocate_align{}; - void* do_deallocate_ptr{}; - size_t do_deallocate_bytes{}; - size_t do_deallocate_align{}; - mutable const arena* do_is_equal_address{}; - -private: - void* do_allocate(size_t bytes, size_t align) THROWS override - { - do_allocate_bytes = bytes; - do_allocate_align = align; - return nullptr; - } - - void do_deallocate(void* ptr, size_t bytes, size_t align) NOEXCEPT override - { - do_deallocate_ptr = ptr; - do_deallocate_bytes = bytes; - do_deallocate_align = align; - } - - bool do_is_equal(const arena& other) const NOEXCEPT override - { - do_is_equal_address = &other; - return false; - } -}; - // arena BOOST_AUTO_TEST_CASE(arena__allocate1__non_zero__expected_bytes_max_align) { - test_arena instance{}; + test::mock_arena instance{}; constexpr auto bytes = 42_size; - BOOST_REQUIRE(is_null(instance.allocate(bytes))); + BOOST_REQUIRE_EQUAL(instance.allocate(bytes), &instance); BOOST_REQUIRE_EQUAL(instance.do_allocate_bytes, bytes); - BOOST_REQUIRE_EQUAL(instance.do_allocate_align, arena::max_align); + BOOST_REQUIRE_EQUAL(instance.do_allocate_align, alignof(max_align_t)); } BOOST_AUTO_TEST_CASE(arena__allocate2__non_zero__expected_bytes_align) { - test_arena instance{}; + test::mock_arena instance{}; constexpr auto bytes = 42_size; constexpr auto align = 4_size; - BOOST_REQUIRE(is_null(instance.allocate(bytes, align))); + BOOST_REQUIRE_EQUAL(instance.allocate(bytes, align), &instance); BOOST_REQUIRE_EQUAL(instance.do_allocate_bytes, bytes); BOOST_REQUIRE_EQUAL(instance.do_allocate_align, align); } BOOST_AUTO_TEST_CASE(arena__deallocate2__nullptr_non_zero__expected_ptr_bytes_max_align) { - test_arena instance{}; + test::mock_arena instance{}; constexpr auto ptr = nullptr; constexpr auto bytes = 42_size; instance.deallocate(ptr, bytes); BOOST_REQUIRE_EQUAL(instance.do_deallocate_ptr, ptr); BOOST_REQUIRE_EQUAL(instance.do_deallocate_bytes, bytes); - BOOST_REQUIRE_EQUAL(instance.do_deallocate_align, arena::max_align); + BOOST_REQUIRE_EQUAL(instance.do_deallocate_align, alignof(max_align_t)); } BOOST_AUTO_TEST_CASE(arena__deallocate2__nullptr_non_zero__expected_ptr_bytes_align) { - test_arena instance{}; + test::mock_arena instance{}; constexpr auto ptr = nullptr; constexpr auto bytes = 42_size; constexpr auto align = 24_size; @@ -102,15 +69,15 @@ BOOST_AUTO_TEST_CASE(arena__deallocate2__nullptr_non_zero__expected_ptr_bytes_al BOOST_AUTO_TEST_CASE(arena__is_equal__different__expected) { - test_arena other{}; - test_arena instance{}; + test::mock_arena other{}; + test::mock_arena instance{}; BOOST_REQUIRE(!instance.is_equal(other)); BOOST_REQUIRE_EQUAL(instance.do_is_equal_address, &other); } BOOST_AUTO_TEST_CASE(arena__is_equal__same__expected) { - test_arena instance{}; + test::mock_arena instance{}; BOOST_REQUIRE(!instance.is_equal(instance)); BOOST_REQUIRE_EQUAL(instance.do_is_equal_address, &instance); } @@ -130,7 +97,7 @@ BOOST_AUTO_TEST_CASE(default_arena__allocate1__non_zero_bytes__non_null_allocati BOOST_AUTO_TEST_CASE(default_arena__allocate2__non_zero_bytes_max_align__non_null_allocation) { default_arena instance{}; - const auto ptr = instance.allocate(42, default_arena::max_align); + const auto ptr = instance.allocate(42, alignof(max_align_t)); BOOST_REQUIRE(!is_null(ptr)); // Clean up unfaked allocation. @@ -148,44 +115,33 @@ BOOST_AUTO_TEST_CASE(default_arena__deallocate1__non_zero_bytes__does_not_throw) { default_arena instance{}; constexpr auto bytes = 42_size; - const auto ptr = ::operator new(bytes); + auto ptr = ::operator new(bytes); BOOST_REQUIRE_NO_THROW(instance.deallocate(ptr, bytes)); + ptr = nullptr; } BOOST_AUTO_TEST_CASE(default_arena__deallocate2__non_zero_bytes_max_align__does_not_throw) { default_arena instance{}; constexpr auto bytes = 42_size; - const auto ptr = ::operator new(bytes); - BOOST_REQUIRE_NO_THROW(instance.deallocate(ptr, bytes, default_arena::max_align)); + auto ptr = ::operator new(bytes); + BOOST_REQUIRE_NO_THROW(instance.deallocate(ptr, bytes, alignof(max_align_t))); + ptr = nullptr; } BOOST_AUTO_TEST_CASE(default_arena__is_equal__same__true) { - test_arena instance{}; + test::mock_arena instance{}; BOOST_REQUIRE(!instance.is_equal(instance)); } BOOST_AUTO_TEST_CASE(default_arena__is_equal__different__false) { - test_arena other{}; - test_arena instance{}; + test::mock_arena other{}; + test::mock_arena instance{}; BOOST_REQUIRE(!instance.is_equal(other)); } -////BOOST_AUTO_TEST_CASE(default_arena__allocate__excess_align__throws_allocation_exception) -////{ -//// default_arena instance{}; -//// BOOST_REQUIRE_THROW(is_null(instance.allocate(one, max_size_t)), allocation_exception); -////} -//// -////BOOST_AUTO_TEST_CASE(default_arena__deallocate__excess_align__throws_allocation_exception) -////{ -//// bool foo{}; -//// default_arena instance{}; -//// BOOST_REQUIRE_THROW(instance.deallocate(&foo, one, max_size_t), allocation_exception); -////} - BC_POP_WARNING() BC_POP_WARNING() diff --git a/test/test.hpp b/test/test.hpp index ab59d1650b..08f04d8b92 100644 --- a/test/test.hpp +++ b/test/test.hpp @@ -152,9 +152,9 @@ class reporting_arena dec_bytes += bytes; } - bool do_is_equal(const arena&) const NOEXCEPT override + bool do_is_equal(const arena& other) const NOEXCEPT override { - return true; + return &other == this; } void report(void* ptr, size_t bytes, bool allocate) const NOEXCEPT @@ -173,6 +173,39 @@ class reporting_arena } }; +class mock_arena + : public arena +{ +public: + size_t do_allocate_bytes{}; + size_t do_allocate_align{}; + void* do_deallocate_ptr{}; + size_t do_deallocate_bytes{}; + size_t do_deallocate_align{}; + mutable const arena* do_is_equal_address{}; + +private: + void* do_allocate(size_t bytes, size_t align) THROWS override + { + do_allocate_bytes = bytes; + do_allocate_align = align; + return this; + } + + void do_deallocate(void* ptr, size_t bytes, size_t align) NOEXCEPT override + { + do_deallocate_ptr = ptr; + do_deallocate_bytes = bytes; + do_deallocate_align = align; + } + + bool do_is_equal(const arena& other) const NOEXCEPT override + { + do_is_equal_address = &other; + return false; + } +}; + template test::reporting_arena* get_test_resource() NOEXCEPT {