[PATCH] libstdc++: Fix constantness of engaged -> disengaged std::optional [PR124910]

Patrick Palka ppalka@redhat.com
Sat Apr 18 01:44:05 GMT 2026


Tested on x86_64-pc-linux-gnu, does this look OK for trunk?  Perhaps
backports as well?

-- >8 --

We implement std::optional<T> for trivially destructible T as a union
with an empty dummy member (_M_empty) indicating the disengaged state.
When we disengage such an std::optional that's already engaged we call
_M_destroy, which clears the _M_engaged flag and invokes the contained
objects's destructor, ending the lifetime of the corresponding union
member (_M_value) and leaving the union with no active member.  While
benign at runtime, a union subobject with no active member violates
core constant expression requirements.  Consequently the resulting
value can't be used as a constant initializer, which Clang and recent
GCC (r16-3022) correctly diagnose.

To fix this, this patch makes _M_destroy activate the _M_empty union
member after ending the destroying and deactivating _M_value.  We use
std::construct_at instead of simple assignment to work around a front
end bug (see comment #6 of the PR).

	PR c++/124910

libstdc++-v3/ChangeLog:

	* include/std/optional (_Optional_payload_base::_M_destroy):
	During constant evaluation, after invoking destructor of
	_M_value, call construct_at to activate _M_empty.
	* testsuite/20_util/optional/constexpr/124910.cc: New test.
---
 libstdc++-v3/include/std/optional             |  2 +
 .../20_util/optional/constexpr/124910.cc      | 63 +++++++++++++++++++
 2 files changed, 65 insertions(+)
 create mode 100644 libstdc++-v3/testsuite/20_util/optional/constexpr/124910.cc

diff --git a/libstdc++-v3/include/std/optional b/libstdc++-v3/include/std/optional
index 0f4cf0bd1ef6..0524222eab97 100644
--- a/libstdc++-v3/include/std/optional
+++ b/libstdc++-v3/include/std/optional
@@ -321,6 +321,8 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
       {
 	_M_engaged = false;
 	_M_payload._M_value.~_Stored_type();
+	if (std::__is_constant_evaluated())
+	  std::construct_at(std::__addressof(_M_payload._M_empty));
       }
 
 #if __cplusplus >= 202002L
diff --git a/libstdc++-v3/testsuite/20_util/optional/constexpr/124910.cc b/libstdc++-v3/testsuite/20_util/optional/constexpr/124910.cc
new file mode 100644
index 000000000000..f7facdec020d
--- /dev/null
+++ b/libstdc++-v3/testsuite/20_util/optional/constexpr/124910.cc
@@ -0,0 +1,63 @@
+// { dg-do compile { target c++20 } }
+
+// PR124910 - bogus 'std::optional{...}' is not a constant expression error
+// after resetting it via '= nullopt'
+
+#include <optional>
+
+struct A {
+  constexpr A(int m) : m(m) { }
+  int m;
+};
+
+struct B {
+  constexpr B(int m) : m(m) { }
+  constexpr ~B() { }
+  int m;
+};
+
+static_assert(   std::is_trivially_destructible_v<int> );
+static_assert(   std::is_trivially_destructible_v<A> );
+static_assert( ! std::is_trivially_destructible_v<B> );
+
+template<class T>
+void do_test() {
+  constexpr std::optional<T> x1 = [] {
+    std::optional<T> o = 1;
+    o = std::nullopt;
+    return o;
+  }();
+
+  constexpr std::optional<T> x2 = [] {
+    std::optional<T> o = 1;
+    o.reset();
+    return o;
+  }();
+
+  constexpr std::optional<T> x3 = [] {
+    std::optional<T> o1 = 1;
+    std::optional<long> o2;
+    o1 = o2;
+    return o1;
+  }();
+
+  constexpr std::optional<T> x4 = [] {
+    std::optional<T> o1 = 1;
+    std::optional<long> o2;
+    o1 = std::move(o2);
+    return o1;
+  }();
+
+  constexpr std::optional<T> x5 = [] {
+    std::optional<T> o1 = 1;
+    std::optional<T> o2;
+    std::swap(o1, o2);
+    return o1;
+  }();
+}
+
+int main() {
+  do_test<int>();
+  do_test<A>();
+  do_test<B>();
+}
-- 
2.54.0.rc1.54.g60f07c4f5c



More information about the Libstdc++ mailing list