diff --git a/cetlvast/suites/unittest/test_variable_length_array_compiles.cpp b/cetlvast/suites/unittest/test_variable_length_array_compiles.cpp index 9f624cd..ce4418c 100644 --- a/cetlvast/suites/unittest/test_variable_length_array_compiles.cpp +++ b/cetlvast/suites/unittest/test_variable_length_array_compiles.cpp @@ -41,7 +41,7 @@ TYPED_TEST(TestVariableLengthArrayCompiles, MoveConstructorIsNoThrow) static_assert(noexcept(TypeParam(std::move(std::declval()))), "Must be no-throw move constructable."); } -// used by MoveAssignmentNoexcept test. +// Used by the allocator-dependent move exception specification tests. template struct FakeAllocator { @@ -56,6 +56,31 @@ struct FakeAllocator }; }; +TYPED_TEST(TestVariableLengthArrayCompiles, MoveConstructorWithAllocatorNoexcept) +{ + using AlwaysEqualPropagating = FakeAllocator; + using AlwaysEqual = FakeAllocator; + using Propagating = FakeAllocator; + using Unequal = FakeAllocator; + + using VLA0 = cetl::VariableLengthArray; + using VLA1 = cetl::VariableLengthArray; + using VLA2 = cetl::VariableLengthArray; + using VLA3 = cetl::VariableLengthArray; + + static_assert(std::is_nothrow_constructible::value, + "Transferring storage must remain noexcept."); + static_assert(std::is_nothrow_constructible::value, + "Transferring storage must remain noexcept."); + static_assert(!std::is_nothrow_constructible::value, + "Propagation on move assignment is irrelevant to construction: a potentially unequal allocator may " + "allocate and throw."); + static_assert(!std::is_nothrow_constructible::value, + "Moving with a potentially unequal allocator may allocate and throw."); + static_assert(std::is_nothrow_move_constructible::value, + "Moving without a supplied allocator must remain noexcept."); +} + #if defined(__GNUG__) # pragma GCC diagnostic push # if __GNUC__ >= 13 diff --git a/cetlvast/suites/unittest/test_variable_length_array_copy_and_move.cpp b/cetlvast/suites/unittest/test_variable_length_array_copy_and_move.cpp index 0d83278..9e46990 100644 --- a/cetlvast/suites/unittest/test_variable_length_array_copy_and_move.cpp +++ b/cetlvast/suites/unittest/test_variable_length_array_copy_and_move.cpp @@ -18,6 +18,160 @@ #include #include #include +#include + +#if defined(__cpp_exceptions) + +namespace +{ +struct MoveConstructorAllocatorState +{ + bool fail_allocation = false; + std::size_t allocation_attempts = 0; + std::size_t outstanding_allocations = 0; +}; + +// Never always-equal. Pocma selects propagate_on_container_move_assignment, which must have no bearing on +// allocator-extended move construction. +template +struct MoveConstructorAllocator +{ + using value_type = T; + using is_always_equal = std::false_type; + using propagate_on_container_move_assignment = Pocma; + + explicit MoveConstructorAllocator(MoveConstructorAllocatorState& state) noexcept + : state_(&state) + { + } + + template + MoveConstructorAllocator(const MoveConstructorAllocator& rhs) noexcept + : state_(rhs.state_) + { + } + + T* allocate(std::size_t count) + { + ++state_->allocation_attempts; + if (state_->fail_allocation) + { + throw std::bad_alloc(); + } + T* const result = std::allocator{}.allocate(count); + ++state_->outstanding_allocations; + return result; + } + + void deallocate(T* pointer, std::size_t count) noexcept + { + if (pointer != nullptr) + { + EXPECT_GT(state_->outstanding_allocations, 0U); + --state_->outstanding_allocations; + std::allocator{}.deallocate(pointer, count); + } + } + + template + bool operator==(const MoveConstructorAllocator& rhs) const noexcept + { + return state_ == rhs.state_; + } + + template + bool operator!=(const MoveConstructorAllocator& rhs) const noexcept + { + return !(*this == rhs); + } + + MoveConstructorAllocatorState* state_; +}; + +template +struct MoveConstructorParams +{ + using value_type = T; + using pocma = Pocma; +}; + +template +class VLAMoveConstructorExceptionTests : public ::testing::Test +{ +protected: + using T = typename Params::value_type; + using Subject = cetl::VariableLengthArray>; + using Allocator = typename Subject::allocator_type; +}; + +using MoveConstructorValueTypes = ::testing::Types, + MoveConstructorParams, + MoveConstructorParams, + MoveConstructorParams>; +TYPED_TEST_SUITE(VLAMoveConstructorExceptionTests, MoveConstructorValueTypes, ); + +TYPED_TEST(VLAMoveConstructorExceptionTests, UnequalAllocatorAllocationFailure) +{ + using Subject = typename TestFixture::Subject; + using Allocator = typename TestFixture::Allocator; + + MoveConstructorAllocatorState source_state; + MoveConstructorAllocatorState destination_state; + { + Subject source{{1, 0, 1}, Allocator{source_state}}; + const auto original_capacity = source.capacity(); + destination_state.fail_allocation = true; + + EXPECT_THROW((Subject{std::move(source), Allocator{destination_state}}), std::bad_alloc); + ASSERT_EQ(source.size(), 3U); + EXPECT_EQ(source.capacity(), original_capacity); + EXPECT_EQ(source[0], 1); + EXPECT_EQ(source[1], 0); + EXPECT_EQ(source[2], 1); + EXPECT_EQ(source_state.outstanding_allocations, 1U); + EXPECT_EQ(destination_state.allocation_attempts, 1U); + EXPECT_EQ(destination_state.outstanding_allocations, 0U); + + // The failed allocation must leave the source available for a later move. + destination_state.fail_allocation = false; + Subject destination{std::move(source), Allocator{destination_state}}; + ASSERT_EQ(destination.size(), 3U); + EXPECT_EQ(destination[0], 1); + EXPECT_EQ(destination[1], 0); + EXPECT_EQ(destination[2], 1); + EXPECT_TRUE(source.empty()); + EXPECT_EQ(source_state.outstanding_allocations, 0U); + EXPECT_EQ(destination_state.allocation_attempts, 2U); + EXPECT_EQ(destination_state.outstanding_allocations, 1U); + } + EXPECT_EQ(source_state.outstanding_allocations, 0U); + EXPECT_EQ(destination_state.outstanding_allocations, 0U); +} + +TYPED_TEST(VLAMoveConstructorExceptionTests, EqualAllocatorDoesNotAllocate) +{ + using Subject = typename TestFixture::Subject; + using Allocator = typename TestFixture::Allocator; + + MoveConstructorAllocatorState state; + { + Subject source{{1, 0, 1}, Allocator{state}}; + state.fail_allocation = true; + Subject destination{std::move(source), Allocator{state}}; + + EXPECT_TRUE(source.empty()); + ASSERT_EQ(destination.size(), 3U); + EXPECT_EQ(destination[0], 1); + EXPECT_EQ(destination[1], 0); + EXPECT_EQ(destination[2], 1); + EXPECT_EQ(state.allocation_attempts, 1U); + EXPECT_EQ(state.outstanding_allocations, 1U); + } + EXPECT_EQ(state.outstanding_allocations, 0U); +} +} // namespace + +#endif // __cpp_exceptions // +---------------------------------------------------------------------------+ // | TEST VALUE TYPES diff --git a/include/cetl/variable_length_array.hpp b/include/cetl/variable_length_array.hpp index e715a85..55d445c 100644 --- a/include/cetl/variable_length_array.hpp +++ b/include/cetl/variable_length_array.hpp @@ -796,11 +796,13 @@ class VariableLengthArrayBase rhs.data_ = nullptr; } + /// Allocator-extended move construction where the allocators are always equal: the storage is simply adopted. + /// Note that propagate_on_container_move_assignment has no bearing on construction. template constexpr VariableLengthArrayBase( VariableLengthArrayBase&& rhs, const UAlloc& rhs_alloc, - typename std::enable_if_t::value>* = nullptr) noexcept + typename std::enable_if_t::is_always_equal::value>* = nullptr) noexcept : alloc_(std::allocator_traits::select_on_container_copy_construction(rhs_alloc)) , data_(std::move(rhs.data_)) , capacity_(rhs.capacity_) @@ -814,11 +816,14 @@ class VariableLengthArrayBase rhs.data_ = nullptr; } + /// Allocator-extended move construction where the allocators may be unequal. If they are equal at runtime the + /// storage is adopted, otherwise storage is obtained from the given allocator and the elements are moved into + /// it. The latter can throw so this overload is not noexcept. template constexpr VariableLengthArrayBase( VariableLengthArrayBase&& rhs, const UAlloc& rhs_alloc, - typename std::enable_if_t::value>* = nullptr) noexcept + typename std::enable_if_t::is_always_equal::value>* = nullptr) : alloc_(std::allocator_traits::select_on_container_copy_construction(rhs_alloc)) , data_{nullptr} , capacity_(0) @@ -1042,7 +1047,8 @@ class VariableLengthArray : protected VariableLengthArrayBase return *this; } - VariableLengthArray(VariableLengthArray&& rhs, const allocator_type& alloc) noexcept + VariableLengthArray(VariableLengthArray&& rhs, const allocator_type& alloc) noexcept( + std::allocator_traits::is_always_equal::value) : Base(std::move(rhs), alloc) { } @@ -1910,7 +1916,8 @@ class VariableLengthArray : protected VariableLengthArrayBase::is_always_equal::value) : Base(std::move(rhs), alloc) , last_byte_bit_fill_{rhs.last_byte_bit_fill_} {