// Copyright 2017 The Abseil Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include "absl/types/variant.h" #include "absl/base/config.h" // This test is a no-op when absl::variant is an alias for std::variant and when // exceptions are not enabled. #if !defined(ABSL_USES_STD_VARIANT) && defined(ABSL_HAVE_EXCEPTIONS) #include #include #include #include #include "gmock/gmock.h" #include "gtest/gtest.h" #include "absl/base/internal/exception_safety_testing.h" #include "absl/memory/memory.h" // See comment in absl/base/config.h #if !defined(ABSL_INTERNAL_MSVC_2017_DBG_MODE) namespace absl { ABSL_NAMESPACE_BEGIN namespace { using ::testing::MakeExceptionSafetyTester; using ::testing::strong_guarantee; using ::testing::TestNothrowOp; using ::testing::TestThrowingCtor; using Thrower = testing::ThrowingValue<>; using CopyNothrow = testing::ThrowingValue; using MoveNothrow = testing::ThrowingValue; using ThrowingAlloc = testing::ThrowingAllocator; using ThrowerVec = std::vector; using ThrowingVariant = absl::variant; struct ConversionException {}; template struct ExceptionOnConversion { operator T() const { // NOLINT throw ConversionException(); } }; // Forces a variant into the valueless by exception state. void ToValuelessByException(ThrowingVariant& v) { // NOLINT try { v.emplace(); v.emplace(ExceptionOnConversion()); } catch (const ConversionException&) { // This space intentionally left blank. } } // Check that variant is still in a usable state after an exception is thrown. testing::AssertionResult VariantInvariants(ThrowingVariant* v) { using testing::AssertionFailure; using testing::AssertionSuccess; // Try using the active alternative if (absl::holds_alternative(*v)) { auto& t = absl::get(*v); t = Thrower{-100}; if (t.Get() != -100) { return AssertionFailure() << "Thrower should be assigned -100"; } } else if (absl::holds_alternative(*v)) { auto& tv = absl::get(*v); tv.clear(); tv.emplace_back(-100); if (tv.size() != 1 || tv[0].Get() != -100) { return AssertionFailure() << "ThrowerVec should be {Thrower{-100}}"; } } else if (absl::holds_alternative(*v)) { auto& t = absl::get(*v); t = CopyNothrow{-100}; if (t.Get() != -100) { return AssertionFailure() << "CopyNothrow should be assigned -100"; } } else if (absl::holds_alternative(*v)) { auto& t = absl::get(*v); t = MoveNothrow{-100}; if (t.Get() != -100) { return AssertionFailure() << "MoveNothrow should be assigned -100"; } } // Try making variant valueless_by_exception if (!v->valueless_by_exception()) ToValuelessByException(*v); if (!v->valueless_by_exception()) { return AssertionFailure() << "Variant should be valueless_by_exception"; } try { auto unused = absl::get(*v); static_cast(unused); return AssertionFailure() << "Variant should not contain Thrower"; } catch (const absl::bad_variant_access&) { } catch (...) { return AssertionFailure() << "Unexpected exception throw from absl::get"; } // Try using the variant v->emplace(100); if (!absl::holds_alternative(*v) || absl::get(*v) != Thrower(100)) { return AssertionFailure() << "Variant should contain Thrower(100)"; } v->emplace({Thrower(100)}); if (!absl::holds_alternative(*v) || absl::get(*v)[0] != Thrower(100)) { return AssertionFailure() << "Variant should contain ThrowerVec{Thrower(100)}"; } return AssertionSuccess(); } template Thrower ExpectedThrower(Args&&... args) { return Thrower(42, args...); } ThrowerVec ExpectedThrowerVec() { return {Thrower(100), Thrower(200)}; } ThrowingVariant ValuelessByException() { ThrowingVariant v; ToValuelessByException(v); return v; } ThrowingVariant WithThrower() { return Thrower(39); } ThrowingVariant WithThrowerVec() { return ThrowerVec{Thrower(1), Thrower(2), Thrower(3)}; } ThrowingVariant WithCopyNoThrow() { return CopyNothrow(39); } ThrowingVariant WithMoveNoThrow() { return MoveNothrow(39); } TEST(VariantExceptionSafetyTest, DefaultConstructor) { TestThrowingCtor(); } TEST(VariantExceptionSafetyTest, CopyConstructor) { { ThrowingVariant v(ExpectedThrower()); TestThrowingCtor(v); } { ThrowingVariant v(ExpectedThrowerVec()); TestThrowingCtor(v); } { ThrowingVariant v(ValuelessByException()); TestThrowingCtor(v); } } TEST(VariantExceptionSafetyTest, MoveConstructor) { { ThrowingVariant v(ExpectedThrower()); TestThrowingCtor(std::move(v)); } { ThrowingVariant v(ExpectedThrowerVec()); TestThrowingCtor(std::move(v)); } { ThrowingVariant v(ValuelessByException()); TestThrowingCtor(std::move(v)); } } TEST(VariantExceptionSafetyTest, ValueConstructor) { TestThrowingCtor(ExpectedThrower()); TestThrowingCtor(ExpectedThrowerVec()); } TEST(VariantExceptionSafetyTest, InPlaceTypeConstructor) { TestThrowingCtor(absl::in_place_type_t{}, ExpectedThrower()); TestThrowingCtor(absl::in_place_type_t{}, ExpectedThrowerVec()); } TEST(VariantExceptionSafetyTest, InPlaceIndexConstructor) { TestThrowingCtor(absl::in_place_index_t<0>{}, ExpectedThrower()); TestThrowingCtor(absl::in_place_index_t<3>{}, ExpectedThrowerVec()); } TEST(VariantExceptionSafetyTest, CopyAssign) { // variant& operator=(const variant& rhs); // Let j be rhs.index() { // - neither *this nor rhs holds a value const ThrowingVariant rhs = ValuelessByException(); ThrowingVariant lhs = ValuelessByException(); EXPECT_TRUE(TestNothrowOp([&]() { lhs = rhs; })); } { // - *this holds a value but rhs does not const ThrowingVariant rhs = ValuelessByException(); ThrowingVariant lhs = WithThrower(); EXPECT_TRUE(TestNothrowOp([&]() { lhs = rhs; })); } // - index() == j { const ThrowingVariant rhs(ExpectedThrower()); auto tester = MakeExceptionSafetyTester() .WithInitialValue(WithThrower()) .WithOperation([&rhs](ThrowingVariant* lhs) { *lhs = rhs; }); EXPECT_TRUE(tester.WithContracts(VariantInvariants).Test()); EXPECT_FALSE(tester.WithContracts(strong_guarantee).Test()); } { const ThrowingVariant rhs(ExpectedThrowerVec()); auto tester = MakeExceptionSafetyTester() .WithInitialValue(WithThrowerVec()) .WithOperation([&rhs](ThrowingVariant* lhs) { *lhs = rhs; }); EXPECT_TRUE(tester.WithContracts(VariantInvariants).Test()); EXPECT_FALSE(tester.WithContracts(strong_guarantee).Test()); } // libstdc++ std::variant has bugs on copy assignment regarding exception // safety. #if !(defined(ABSL_USES_STD_VARIANT) && defined(__GLIBCXX__)) // index() != j // if is_nothrow_copy_constructible_v or // !is_nothrow_move_constructible is true, equivalent to // emplace(get(rhs)) { // is_nothrow_copy_constructible_v == true // should not throw because emplace() invokes Tj's copy ctor // which should not throw. const ThrowingVariant rhs(CopyNothrow{}); ThrowingVariant lhs = WithThrower(); EXPECT_TRUE(TestNothrowOp([&]() { lhs = rhs; })); } { // is_nothrow_copy_constructible == false && // is_nothrow_move_constructible == false // should provide basic guarantee because emplace() invokes Tj's copy ctor // which may throw. const ThrowingVariant rhs(ExpectedThrower()); auto tester = MakeExceptionSafetyTester() .WithInitialValue(WithCopyNoThrow()) .WithOperation([&rhs](ThrowingVariant* lhs) { *lhs = rhs; }); EXPECT_TRUE(tester .WithContracts(VariantInvariants, [](ThrowingVariant* lhs) { return lhs->valueless_by_exception(); }) .Test()); EXPECT_FALSE(tester.WithContracts(strong_guarantee).Test()); } #endif // !(defined(ABSL_USES_STD_VARIANT) && defined(__GLIBCXX__)) { // is_nothrow_copy_constructible_v == false && // is_nothrow_move_constructible_v == true // should provide strong guarantee because it is equivalent to // operator=(variant(rhs)) which creates a temporary then invoke the move // ctor which shouldn't throw. const ThrowingVariant rhs(MoveNothrow{}); EXPECT_TRUE(MakeExceptionSafetyTester() .WithInitialValue(WithThrower()) .WithContracts(VariantInvariants, strong_guarantee) .Test([&rhs](ThrowingVariant* lhs) { *lhs = rhs; })); } } TEST(VariantExceptionSafetyTest, MoveAssign) { // variant& operator=(variant&& rhs); // Let j be rhs.index() { // - neither *this nor rhs holds a value ThrowingVariant rhs = ValuelessByException(); ThrowingVariant lhs = ValuelessByException(); EXPECT_TRUE(TestNothrowOp([&]() { lhs = std::move(rhs); })); } { // - *this holds a value but rhs does not ThrowingVariant rhs = ValuelessByException(); ThrowingVariant lhs = WithThrower(); EXPECT_TRUE(TestNothrowOp([&]() { lhs = std::move(rhs); })); } { // - index() == j // assign get(std::move(rhs)) to the value contained in *this. // If an exception is thrown during call to Tj's move assignment, the state // of the contained value is as defined by the exception safety guarantee of // Tj's move assignment; index() will be j. ThrowingVariant rhs(ExpectedThrower()); size_t j = rhs.index(); // Since Thrower's move assignment has basic guarantee, so should variant's. auto tester = MakeExceptionSafetyTester() .WithInitialValue(WithThrower()) .WithOperation([&](ThrowingVariant* lhs) { auto copy = rhs; *lhs = std::move(copy); }); EXPECT_TRUE(tester .WithContracts( VariantInvariants, [&](ThrowingVariant* lhs) { return lhs->index() == j; }) .Test()); EXPECT_FALSE(tester.WithContracts(strong_guarantee).Test()); } { // libstdc++ introduced a regression between 2018-09-25 and 2019-01-06. // The fix is targeted for gcc-9. // https://gcc.gnu.org/bugzilla/show_bug.cgi?id=87431#c7 // https://gcc.gnu.org/viewcvs/gcc?view=revision&revision=267614 #if !(defined(ABSL_USES_STD_VARIANT) && \ defined(_GLIBCXX_RELEASE) && _GLIBCXX_RELEASE == 8) // - otherwise (index() != j), equivalent to // emplace(get(std::move(rhs))) // - If an exception is thrown during the call to Tj's move construction // (with j being rhs.index()), the variant will hold no value. ThrowingVariant rhs(CopyNothrow{}); EXPECT_TRUE(MakeExceptionSafetyTester() .WithInitialValue(WithThrower()) .WithContracts(VariantInvariants, [](ThrowingVariant* lhs) { return lhs->valueless_by_exception(); }) .Test([&](ThrowingVariant* lhs) { auto copy = rhs; *lhs = std::move(copy); })); #endif // !(defined(ABSL_USES_STD_VARIANT) && // defined(_GLIBCXX_RELEASE) && _GLIBCXX_RELEASE == 8) } } TEST(VariantExceptionSafetyTest, ValueAssign) { // template variant& operator=(T&& t); // Let Tj be the type that is selected by overload resolution to be assigned. { // If *this holds a Tj, assigns std::forward(t) to the value contained in // *this. If an exception is thrown during the assignment of // std::forward(t) to the value contained in *this, the state of the // contained value and t are as defined by the exception safety guarantee of // the assignment expression; valueless_by_exception() will be false. // Since Thrower's copy/move assignment has basic guarantee, so should // variant's. Thrower rhs = ExpectedThrower(); // copy assign auto copy_tester = MakeExceptionSafetyTester() .WithInitialValue(WithThrower()) .WithOperation([rhs](ThrowingVariant* lhs) { *lhs = rhs; }); EXPECT_TRUE(copy_tester .WithContracts(VariantInvariants, [](ThrowingVariant* lhs) { return !lhs->valueless_by_exception(); }) .Test()); EXPECT_FALSE(copy_tester.WithContracts(strong_guarantee).Test()); // move assign auto move_tester = MakeExceptionSafetyTester() .WithInitialValue(WithThrower()) .WithOperation([&](ThrowingVariant* lhs) { auto copy = rhs; *lhs = std::move(copy); }); EXPECT_TRUE(move_tester .WithContracts(VariantInvariants, [](ThrowingVariant* lhs) { return !lhs->valueless_by_exception(); }) .Test()); EXPECT_FALSE(move_tester.WithContracts(strong_guarantee).Test()); } // Otherwise (*this holds something else), if is_nothrow_constructible_v || !is_nothrow_move_constructible_v is true, equivalent to // emplace(std::forward(t)). // We simplify the test by letting T = `const Tj&` or `Tj&&`, so we can reuse // the CopyNothrow and MoveNothrow types. // if is_nothrow_constructible_v // (i.e. is_nothrow_copy/move_constructible_v) is true, emplace() just // invokes the copy/move constructor and it should not throw. { const CopyNothrow rhs; ThrowingVariant lhs = WithThrower(); EXPECT_TRUE(TestNothrowOp([&]() { lhs = rhs; })); } { MoveNothrow rhs; ThrowingVariant lhs = WithThrower(); EXPECT_TRUE(TestNothrowOp([&]() { lhs = std::move(rhs); })); } // if is_nothrow_constructible_v == false && // is_nothrow_move_constructible == false // emplace() invokes the copy/move constructor which may throw so it should // provide basic guarantee and variant object might not hold a value. { Thrower rhs = ExpectedThrower(); // copy auto copy_tester = MakeExceptionSafetyTester() .WithInitialValue(WithCopyNoThrow()) .WithOperation([&rhs](ThrowingVariant* lhs) { *lhs = rhs; }); EXPECT_TRUE(copy_tester .WithContracts(VariantInvariants, [](ThrowingVariant* lhs) { return lhs->valueless_by_exception(); }) .Test()); EXPECT_FALSE(copy_tester.WithContracts(strong_guarantee).Test()); // move auto move_tester = MakeExceptionSafetyTester() .WithInitialValue(WithCopyNoThrow()) .WithOperation([](ThrowingVariant* lhs) { *lhs = ExpectedThrower(testing::nothrow_ctor); }); EXPECT_TRUE(move_tester .WithContracts(VariantInvariants, [](ThrowingVariant* lhs) { return lhs->valueless_by_exception(); }) .Test()); EXPECT_FALSE(move_tester.WithContracts(strong_guarantee).Test()); } // Otherwise (if is_nothrow_constructible_v == false && // is_nothrow_move_constructible == true), // equivalent to operator=(variant(std::forward(t))) // This should have strong guarantee because it creates a temporary variant // and operator=(variant&&) invokes Tj's move ctor which doesn't throw. // libstdc++ std::variant has bugs on conversion assignment regarding // exception safety. #if !(defined(ABSL_USES_STD_VARIANT) && defined(__GLIBCXX__)) { MoveNothrow rhs; EXPECT_TRUE(MakeExceptionSafetyTester() .WithInitialValue(WithThrower()) .WithContracts(VariantInvariants, strong_guarantee) .Test([&rhs](ThrowingVariant* lhs) { *lhs = rhs; })); } #endif // !(defined(ABSL_USES_STD_VARIANT) && defined(__GLIBCXX__)) } TEST(VariantExceptionSafetyTest, Emplace) { // If an exception during the initialization of the contained value, the // variant might not hold a value. The standard requires emplace() to provide // only basic guarantee. { Thrower args = ExpectedThrower(); auto tester = MakeExceptionSafetyTester() .WithInitialValue(WithThrower()) .WithOperation([&args](ThrowingVariant* v) { v->emplace(args); }); EXPECT_TRUE(tester .WithContracts(VariantInvariants, [](ThrowingVariant* v) { return v->valueless_by_exception(); }) .Test()); EXPECT_FALSE(tester.WithContracts(strong_guarantee).Test()); } } TEST(VariantExceptionSafetyTest, Swap) { // if both are valueless_by_exception(), no effect { ThrowingVariant rhs = ValuelessByException(); ThrowingVariant lhs = ValuelessByException(); EXPECT_TRUE(TestNothrowOp([&]() { lhs.swap(rhs); })); } // if index() == rhs.index(), calls swap(get(*this), get(rhs)) // where i is index(). { ThrowingVariant rhs = ExpectedThrower(); EXPECT_TRUE(MakeExceptionSafetyTester() .WithInitialValue(WithThrower()) .WithContracts(VariantInvariants) .Test([&](ThrowingVariant* lhs) { auto copy = rhs; lhs->swap(copy); })); } // Otherwise, exchanges the value of rhs and *this. The exception safety // involves variant in moved-from state which is not specified in the // standard, and since swap is 3-step it's impossible for it to provide a // overall strong guarantee. So, we are only checking basic guarantee here. { ThrowingVariant rhs = ExpectedThrower(); EXPECT_TRUE(MakeExceptionSafetyTester() .WithInitialValue(WithCopyNoThrow()) .WithContracts(VariantInvariants) .Test([&](ThrowingVariant* lhs) { auto copy = rhs; lhs->swap(copy); })); } { ThrowingVariant rhs = ExpectedThrower(); EXPECT_TRUE(MakeExceptionSafetyTester() .WithInitialValue(WithCopyNoThrow()) .WithContracts(VariantInvariants) .Test([&](ThrowingVariant* lhs) { auto copy = rhs; copy.swap(*lhs); })); } } } // namespace ABSL_NAMESPACE_END } // namespace absl #endif // !defined(ABSL_INTERNAL_MSVC_2017_DBG_MODE) #endif // #if !defined(ABSL_USES_STD_VARIANT) && defined(ABSL_HAVE_EXCEPTIONS)