From 1ef585f224befa34267178c09d719d7df4802992 Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Sat, 2 Mar 2024 23:22:20 +0200 Subject: [PATCH] Assignment 2 --- .../suites/unittest/test_pf17_variant.cpp | 325 ++++++++++++++++++ include/cetl/pf17/variant.hpp | 10 +- 2 files changed, 330 insertions(+), 5 deletions(-) diff --git a/cetlvast/suites/unittest/test_pf17_variant.cpp b/cetlvast/suites/unittest/test_pf17_variant.cpp index e7e0fc08..43e09b78 100644 --- a/cetlvast/suites/unittest/test_pf17_variant.cpp +++ b/cetlvast/suites/unittest/test_pf17_variant.cpp @@ -654,6 +654,7 @@ struct test_assignment_1 static void test_matching_assignment_throwing() { +#if __cpp_exceptions using cetl::pf17::variant; using cetl::pf17::get; using cetlvast::smf_policies::policy_nontrivial; @@ -691,6 +692,7 @@ struct test_assignment_1 } EXPECT_EQ((T::dtor_policy_value == policy_nontrivial) ? 1 : 0, dtor1); // Assignment did not succeed. EXPECT_EQ((T::dtor_policy_value == policy_nontrivial) ? 1 : 0, dtor2); +#endif } static void test_nonmatching_copy_noexcept_move_noexcept() @@ -972,3 +974,326 @@ TYPED_TEST(test_smf_policy_combinations, assignment_1) { test_assignment_1::test(); } + +// -------------------------------------------------------------------------------------------- + +/// For the move assignment to work, every T in Ts shall be both +/// (move-constructible or copy-constructible) and (move-assignable or copy-assignable). +/// Notes: +/// - A type does not have to implement move assignment operator in order to satisfy MoveAssignable: +/// a copy assignment operator that takes its parameter by value or as a const Type&, will bind to rvalue argument. +/// - A type does not have to implement a move constructor to satisfy MoveConstructible: +/// a copy constructor that takes a const T& argument can bind rvalue expressions. +template +struct test_assignment_2 +{ + static void test_matching_assignment_noexcept() + { + using cetl::pf17::variant; + using cetl::pf17::get; + using cetlvast::smf_policies::policy_nontrivial; + using cetlvast::smf_policies::policy_deleted; + std::uint32_t dtor1 = 0; + std::uint32_t dtor2 = 0; + { + variant v1; + variant v2; + get(v1).configure_destruction_counter(&dtor1); + get(v2).configure_destruction_counter(&dtor2); + v2 = std::move(v1); // Invoke copy assignment. + EXPECT_FALSE(v1.valueless_by_exception()); // NOLINT(*-use-after-move) + EXPECT_FALSE(v2.valueless_by_exception()); + // Check v1 counters. + EXPECT_EQ(0U, get(v1).get_copy_ctor_count()); + EXPECT_EQ(0U, get(v1).get_move_ctor_count()); + EXPECT_EQ(0U, get(v1).get_copy_assignment_count()); + EXPECT_EQ(0U, get(v1).get_move_assignment_count()); + EXPECT_EQ(0, dtor1); + // Check v2 counters. + EXPECT_EQ(0U, get(v2).get_copy_ctor_count()); + EXPECT_EQ(0U, get(v2).get_move_ctor_count()); + EXPECT_EQ(((T::copy_assignment_policy_value == policy_nontrivial) && + (T::move_assignment_policy_value == policy_deleted)) + ? 1 + : 0, + get(v2).get_copy_assignment_count()); + EXPECT_EQ((T::move_assignment_policy_value == policy_nontrivial) ? 1 : 0, + get(v2).get_move_assignment_count()); + EXPECT_EQ(0, dtor2); + } + EXPECT_EQ((T::dtor_policy_value == policy_nontrivial) ? 2 : 0, dtor1); + EXPECT_EQ(0, dtor2); // This is because of the assignment. + } + + static void test_matching_assignment_throwing() + { +#if __cpp_exceptions + using cetl::pf17::variant; + using cetl::pf17::get; + using cetlvast::smf_policies::policy_deleted; + using cetlvast::smf_policies::policy_nontrivial; + struct U : T + { + U() = default; + U(const U&) noexcept = default; + U(U&&) noexcept = default; + U& operator=(const U&) noexcept = default; + U& operator=(U&&) // NOLINT(*-noexcept-move-constructor) + { + throw std::exception(); + } + }; + std::uint32_t dtor1 = 0; + std::uint32_t dtor2 = 0; + { + variant v1; + variant v2; + get(v1).configure_destruction_counter(&dtor1); + get(v2).configure_destruction_counter(&dtor2); + EXPECT_ANY_THROW(v2 = std::move(v1)); // Invoke move assignment. It will throw. + // Destination does not become valueless despite the exception. + EXPECT_FALSE(v1.valueless_by_exception()); // NOLINT(*-use-after-move) + EXPECT_FALSE(v2.valueless_by_exception()); + // Check v1 counters. + EXPECT_EQ(0U, get(v1).get_copy_ctor_count()); + EXPECT_EQ(0U, get(v1).get_move_ctor_count()); + EXPECT_EQ(0U, get(v1).get_copy_assignment_count()); + EXPECT_EQ(0U, get(v1).get_move_assignment_count()); + EXPECT_EQ(0U, dtor1); + // Check v2 counters. + EXPECT_EQ(0U, get(v2).get_copy_ctor_count()); + EXPECT_EQ(0U, get(v2).get_move_ctor_count()); + EXPECT_EQ(0U, get(v2).get_copy_assignment_count()); // Assignment did not succeed. + EXPECT_EQ(0U, get(v2).get_move_assignment_count()); // Assignment did not succeed. + EXPECT_EQ(0U, dtor2); + } + EXPECT_EQ((T::dtor_policy_value == policy_nontrivial) ? 1 : 0, dtor1); // Assignment did not succeed. + EXPECT_EQ((T::dtor_policy_value == policy_nontrivial) ? 1 : 0, dtor2); +#endif + } + + static void test_nonmatching_noexcept() + { + using cetl::pf17::variant; + using cetl::pf17::get; + using cetl::pf17::in_place_type; + using cetlvast::smf_policies::policy_deleted; + using cetlvast::smf_policies::policy_nontrivial; + struct U : T + {}; + std::uint32_t dtor1 = 0; + std::uint32_t dtor2 = 0; + { + variant v1(in_place_type); + variant v2(in_place_type); + get(v1).configure_destruction_counter(&dtor1); + get(v2).configure_destruction_counter(&dtor2); + v2 = std::move(v1); // Invoke move construction. + EXPECT_FALSE(v1.valueless_by_exception()); // NOLINT(*-use-after-move) + EXPECT_FALSE(v2.valueless_by_exception()); + EXPECT_EQ(0, v1.index()); + EXPECT_EQ(0, v2.index()); + // Check v1 counters. + EXPECT_EQ(0U, get(v1).get_copy_ctor_count()); + EXPECT_EQ(0U, get(v1).get_move_ctor_count()); + EXPECT_EQ(0U, get(v1).get_copy_assignment_count()); + EXPECT_EQ(0U, get(v1).get_move_assignment_count()); + EXPECT_EQ(0U, dtor1); + // Check v2 counters. + EXPECT_EQ(((T::copy_ctor_policy_value == policy_nontrivial) && + (T::move_ctor_policy_value == policy_deleted)) + ? 1 + : 0, + get(v2).get_copy_ctor_count()); + EXPECT_EQ((T::move_ctor_policy_value == policy_nontrivial) ? 1 : 0, get(v2).get_move_ctor_count()); + EXPECT_EQ(0U, get(v2).get_copy_assignment_count()); + EXPECT_EQ(0U, get(v2).get_move_assignment_count()); + EXPECT_EQ((T::dtor_policy_value == policy_nontrivial) ? 1 : 0, dtor2); // T destroyed. + } + EXPECT_EQ((T::dtor_policy_value == policy_nontrivial) ? 2 : 0, dtor1); + EXPECT_EQ((T::dtor_policy_value == policy_nontrivial) ? 1 : 0, dtor2); + } + + static void test_nonmatching_throwing() + { +#if __cpp_exceptions + using cetl::pf17::variant; + using cetl::pf17::variant_npos; + using cetl::pf17::get; + using cetl::pf17::in_place_type; + using cetlvast::smf_policies::policy_deleted; + using cetlvast::smf_policies::policy_nontrivial; + struct U : T + { + U() = default; + U(const U&) noexcept = default; + U(U&& other) // NOLINT(*-noexcept-move-constructor) + : T(std::move(other)) // This resolves either to the move ctor or copy ctor! + { + throw std::exception(); + } + U& operator=(const U&) noexcept = default; + U& operator=(U&&) noexcept = default; + }; + std::uint32_t dtor1 = 0; + std::uint32_t dtor2 = 0; + { + variant v1(in_place_type); + variant v2(in_place_type); + get(v1).configure_destruction_counter(&dtor1); + get(v2).configure_destruction_counter(&dtor2); + EXPECT_ANY_THROW(v2 = std::move(v1)); // Invoke move construction. + EXPECT_FALSE(v1.valueless_by_exception()); // NOLINT(*-use-after-move) + EXPECT_TRUE(v2.valueless_by_exception()); // v2 is valueless because the move ctor of U throws. + EXPECT_EQ(0, v1.index()); + EXPECT_EQ(variant_npos, v2.index()); + // Check v1 counters. + EXPECT_EQ(0U, get(v1).get_copy_ctor_count()); + EXPECT_EQ(0U, get(v1).get_move_ctor_count()); + EXPECT_EQ(0U, get(v1).get_copy_assignment_count()); + EXPECT_EQ(0U, get(v1).get_move_assignment_count()); + // The dtor counter is 1 because the base which does the counting is already constructed by the time + // the exception is thrown, hence its destructor is invoked, which increments the counter. + EXPECT_EQ((T::dtor_policy_value == policy_nontrivial) ? 1 : 0, dtor1); + // v2 counters cannot be checked because it is valueless, except for the dtor counter. + EXPECT_EQ((T::dtor_policy_value == policy_nontrivial) ? 1 : 0, dtor2); + } + EXPECT_EQ((T::dtor_policy_value == policy_nontrivial) ? 2 : 0, dtor1); + EXPECT_EQ((T::dtor_policy_value == policy_nontrivial) ? 1 : 0, dtor2); +#endif + } + + static void test_valueless() + { +#if __cpp_exceptions + using cetl::pf17::variant; + using cetl::pf17::get; + using cetl::pf17::in_place_type; + using cetlvast::smf_policies::policy_deleted; + using cetlvast::smf_policies::policy_nontrivial; + struct U : T + { + U() + { + throw std::exception(); + } + }; + std::uint32_t dtor1 = 0; + std::uint32_t dtor2 = 0; + { + variant v1; + variant v2; + // Make v1 valueless because the ctor of U throws. + EXPECT_ANY_THROW(v1.template emplace()); + EXPECT_TRUE(v1.valueless_by_exception()); + EXPECT_FALSE(v2.valueless_by_exception()); + // Move valueless into non-valueless. + v2 = std::move(v1); + EXPECT_TRUE(v1.valueless_by_exception()); // NOLINT(*-use-after-move) + EXPECT_TRUE(v2.valueless_by_exception()); + // Move valueless into valueless. + v1 = std::move(v2); + EXPECT_TRUE(v1.valueless_by_exception()); + EXPECT_TRUE(v2.valueless_by_exception()); // NOLINT(*-use-after-move) + // Make v2 non-valueless, then move that into v1. + v2.template emplace(); + get(v2).configure_destruction_counter(&dtor2); + v1 = std::move(v2); + get(v1).configure_destruction_counter(&dtor1); + EXPECT_FALSE(v1.valueless_by_exception()); + EXPECT_FALSE(v2.valueless_by_exception()); // NOLINT(*-use-after-move) + EXPECT_EQ(0, dtor1); + EXPECT_EQ(0, dtor2); + EXPECT_EQ(0, v1.index()); + EXPECT_EQ(0, v2.index()); + EXPECT_EQ(((T::copy_ctor_policy_value == policy_nontrivial) && + (T::move_ctor_policy_value == policy_deleted)) + ? 1 + : 0, + get(v1).get_copy_ctor_count()); + EXPECT_EQ((T::move_ctor_policy_value == policy_nontrivial) ? 1 : 0, get(v1).get_move_ctor_count()); + EXPECT_EQ(0, get(v1).get_copy_assignment_count()); + EXPECT_EQ(0, get(v1).get_move_assignment_count()); + EXPECT_EQ(0, get(v2).get_copy_ctor_count()); + EXPECT_EQ(0, get(v2).get_move_ctor_count()); + EXPECT_EQ(0, get(v2).get_copy_assignment_count()); + EXPECT_EQ(0, get(v2).get_move_assignment_count()); + } + EXPECT_EQ((T::dtor_policy_value == policy_nontrivial) ? 1 : 0, dtor1); + EXPECT_EQ((T::dtor_policy_value == policy_nontrivial) ? 1 : 0, dtor2); +#endif + } + + static void test() + { + test_matching_assignment_noexcept(); + test_matching_assignment_throwing(); + test_nonmatching_noexcept(); + test_nonmatching_throwing(); + test_valueless(); + } +}; +template +struct test_assignment_2 +{ + static_assert(std::is_copy_assignable::value == (CopyAssignmentPolicy != cetlvast::smf_policies::policy_deleted), + ""); + static_assert(!std::is_copy_constructible::value, ""); + static_assert(std::is_move_assignable::value, ""); // requires either copy or move assignment + static_assert(!std::is_move_constructible::value, ""); + static_assert(!std::is_copy_constructible>::value, ""); + static_assert(!std::is_copy_assignable>::value, ""); + static_assert(!std::is_move_constructible>::value, ""); + static_assert(!std::is_move_assignable>::value, ""); + static void test() {} +}; +template +struct test_assignment_2 +{ + static_assert(!std::is_copy_assignable::value, ""); + static_assert(std::is_copy_constructible::value == (CopyCtorPolicy != cetlvast::smf_policies::policy_deleted), + ""); + static_assert(!std::is_move_assignable::value, ""); + static_assert(std::is_move_constructible::value, ""); // requires either copy or move ctor + static_assert(std::is_copy_constructible>::value == + (CopyCtorPolicy != cetlvast::smf_policies::policy_deleted), + ""); + static_assert(!std::is_copy_assignable>::value, ""); + static_assert(std::is_move_constructible>::value, ""); // requires either copy or move ctor + static_assert(!std::is_move_assignable>::value, ""); + static void test() {} +}; +template +struct test_assignment_2 +{ + static_assert(!std::is_copy_assignable::value, ""); + static_assert(!std::is_copy_constructible::value, ""); + static_assert(!std::is_move_assignable::value, ""); + static_assert(!std::is_move_constructible::value, ""); + static_assert(!std::is_copy_constructible>::value, ""); + static_assert(!std::is_copy_assignable>::value, ""); + static_assert(!std::is_move_constructible>::value, ""); + static_assert(!std::is_move_assignable>::value, ""); + static void test() {} +}; + +TYPED_TEST(test_smf_policy_combinations, assignment_2) +{ + test_assignment_2::test(); +} diff --git a/include/cetl/pf17/variant.hpp b/include/cetl/pf17/variant.hpp index 78556153..bd8a3b94 100644 --- a/include/cetl/pf17/variant.hpp +++ b/include/cetl/pf17/variant.hpp @@ -427,15 +427,15 @@ struct base_move_construction, smf_deleted> : base_copy_constructio /// - Otherwise, if the alternative held by rhs is either nothrow copy constructible or not /// nothrow move constructible (as determined by std::is_nothrow_copy_constructible and /// std::is_nothrow_move_constructible, respectively), equivalent to -/// this->emplace(*std::get_if(std::addressof(rhs))). +/// `this->emplace(*std::get_if(std::addressof(rhs)))`. /// *this may become valueless_by_exception if an exception is thrown on the copy-construction /// inside emplace. /// -/// - Otherwise, equivalent to this->operator=(variant(rhs)). +/// - Otherwise, equivalent to `this->operator=(variant(rhs))`. /// /// The meaning of the last two cases is that we want to minimize the likelihood of the valueless outcome. -/// If T is nothrow copyable, we simply invoke the copy ctor via construct<>(); otherwise, if T is not nothrow move -/// constructible, there's no way to do it better so we do the same thing -- invoke the copy ctor via construct<>(). +/// If T is nothrow copyable, we simply invoke the copy ctor via `construct<>()`; otherwise, if T is not nothrow move +/// constructible, there's no way to do it better so we do the same thing -- invoke the copy ctor via `construct<>()`. /// However, if T is nothrow move constructible, we can do better by creating a temporary copy on the side, /// which can throw safely without the risk of making this valueless, and then (if that succeeded) we /// nothrow-move it into this. @@ -550,7 +550,7 @@ struct base_move_assignment, smf_nontrivial> : base_copy_assignment // needs to be replaced. If an exception is thrown, *this becomes valueless inside construct(). other.chronomorphize([this, &other](const auto index) { assert(index.value == other.m_index); - this->construct(std::move(other.template as())); + this->template construct(std::move(other.template as())); }); } else