From 0f0102ae766d164fcf98adb957eae1580b5a975d Mon Sep 17 00:00:00 2001
From: Sam Yates <halfflat@gmail.com>
Date: Mon, 7 Sep 2020 20:26:38 +0200
Subject: [PATCH] Fix bugs in util::variant assignment implementation. (#1137)

* Fix copy-paste error that led to legitimate copies throwing a bad_variant_access exception.
* Fix exception safety for the case when an error thrown in copy assignment takes the lhs to valueless.
* Fix wrong valueless semantics associated with an exception being thrown during move assignment.
* Add unit tests that exercise these issues.

Fixes #1136.
---
 arbor/include/arbor/util/variant.hpp | 14 +++--
 test/unit/test_variant.cpp           | 80 ++++++++++++++++++++++++++++
 2 files changed, 89 insertions(+), 5 deletions(-)

diff --git a/arbor/include/arbor/util/variant.hpp b/arbor/include/arbor/util/variant.hpp
index 22fec8f1..1f3ca0ca 100644
--- a/arbor/include/arbor/util/variant.hpp
+++ b/arbor/include/arbor/util/variant.hpp
@@ -218,7 +218,6 @@ struct variant_dynamic_impl<H, T...> {
         else {
             variant_dynamic_impl<T...>::assign(i-1, data, from);
         }
-        if (i!=std::size_t(-1)) throw bad_variant_access{};
     }
 
     static void move_assign(std::size_t i, char* data, const char* from) {
@@ -344,10 +343,17 @@ struct variant {
             }
         }
         else {
+            auto old_which = which_;
             which_ = npos;
             if (x.which_!=npos) {
-                variant_dynamic_impl<T...>::assign(x.which_, data, x.data);
-                which_ = x.which_;
+                try {
+                    variant_dynamic_impl<T...>::assign(x.which_, data, x.data);
+                    which_ = x.which_;
+                }
+                catch (...) {
+                    variant_dynamic_impl<T...>::destroy(old_which, data);
+                    throw;
+                }
             }
         }
         return *this;
@@ -365,10 +371,8 @@ struct variant {
             }
         }
         else {
-            which_ = npos;
             if (x.which_!=npos) {
                 variant_dynamic_impl<T...>::move_assign(x.which_, data, x.data);
-                which_ = x.which_;
             }
         }
         return *this;
diff --git a/test/unit/test_variant.cpp b/test/unit/test_variant.cpp
index 50708993..8fe0e941 100644
--- a/test/unit/test_variant.cpp
+++ b/test/unit/test_variant.cpp
@@ -270,6 +270,86 @@ TEST(variant, valueless) {
     EXPECT_EQ(std::size_t(-1), vi.index());
 }
 
+namespace {
+struct nope {};
+
+struct maybe_throws_on_assign {
+    int i;
+    explicit maybe_throws_on_assign(int i): i(i) {}
+    maybe_throws_on_assign(const maybe_throws_on_assign& x): i(x.i) {}
+    maybe_throws_on_assign(maybe_throws_on_assign&& x): i(x.i) {}
+
+    maybe_throws_on_assign& operator=(const maybe_throws_on_assign& x) {
+        if (x.i<0) throw nope{};
+        i = x.i;
+        return *this;
+    }
+
+    maybe_throws_on_assign& operator=(maybe_throws_on_assign&& x) {
+        if (x.i<0) throw nope{};
+        i = x.i;
+        return *this;
+    }
+
+    ~maybe_throws_on_assign() { ++dtor; }
+
+    static int dtor;
+};
+
+int maybe_throws_on_assign::dtor = 0;
+} // anonymous namespace
+
+TEST(variant, copy_assign) {
+    struct X {
+        X& operator=(const X&) { throw nope{}; }
+    };
+
+    using vidX = variant<int, double, X>;
+
+    vidX v0{in_place_type<int>(), 3};
+    vidX v1{in_place_type<double>(), 4.};
+
+    vidX valueless{X{}};
+    try { valueless = valueless; } catch (...) {}
+    ASSERT_TRUE(valueless.valueless_by_exception());
+
+    vidX v;
+    v = v0;
+    ASSERT_EQ(0u, v.index());
+    EXPECT_EQ(3, get<0>(v));
+
+    v = v1;
+    ASSERT_EQ(1u, v.index());
+    EXPECT_EQ(4., get<1>(v));
+
+    v = valueless;
+    EXPECT_TRUE(v.valueless_by_exception());
+
+    v = v1;
+    ASSERT_EQ(1u, v.index());
+    EXPECT_EQ(4., get<1>(v));
+
+    using vbM = variant<bool, maybe_throws_on_assign>;
+    maybe_throws_on_assign::dtor = 0;
+
+    vbM w0(in_place_type<maybe_throws_on_assign>(), 2);
+    vbM w1(in_place_type<maybe_throws_on_assign>(), -2);
+    ASSERT_EQ(0, maybe_throws_on_assign::dtor);
+
+    EXPECT_THROW(w0 = w1, nope);
+    EXPECT_TRUE(w0.valueless_by_exception());
+    EXPECT_EQ(1, maybe_throws_on_assign::dtor);
+
+    maybe_throws_on_assign::dtor = 0;
+    vbM w2(in_place_type<maybe_throws_on_assign>(), 2);
+
+    ASSERT_EQ(0, maybe_throws_on_assign::dtor);
+
+    EXPECT_THROW(w2 = std::move(w1), nope);
+    EXPECT_FALSE(w2.valueless_by_exception());
+    EXPECT_EQ(0, maybe_throws_on_assign::dtor);
+}
+
 TEST(variant, equality) {
     struct X {
         int i;
-- 
GitLab