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

Patrick Palka ppalka@redhat.com
Sat Apr 18 23:28:07 GMT 2026


Changes in v2:
  - Added code comment
  - Clarified commit message, removed red herring about trivial
    destructibility.
  - Extended test with 'x6' direct-initialization that avoids invoking
    optional's move constructor.  This testcase, when instantiated with T=B,
    demonstrates that non-trivially-copyable optional is also affected
    and we must set _M_empty even in that case.

-- >8 --

When an optional that contains a value is cleared, _M_destroy invokes the
destructor of the contained value _Storage::_M_value, leaving the union
_Storage without an active member.  While this is benign at runtime, a
union suboject with no active member violates core constant expression
requirements and in turn an optional in this state can't be used as a
constant initializer, which Clang and recent GCC (since r16-3022) correctly
diagnose.

To fix this, this patch makes _M_destroy activate the dummy union member
_M_empty after destroying _M_value to ensure that the union always has
an active member throughout its lifetime.  We use std::construct_at
instead of simple assignment to work around a front end bug (comment #3
in the PR).  Doing so means we don't activate the member in C++17 mode,
which should be fine; I don't think it's possible to disengage an engaged
optional using only the C++17 constexpr optional operations.

	PR c++/124910

libstdc++-v3/ChangeLog:

	* include/std/optional (_Optional_payload_base::_M_destroy)
	[__cpp_lib_optional >= 202106L]: During constant evaluation,
	after invoking destructor of _M_value, use construct_at to
	activate _M_empty.
	* testsuite/20_util/optional/constexpr/124910.cc: New test.

Reviewed-by: Jonathan Wakely <jwakely@redhat.com>
---
 libstdc++-v3/include/std/optional             |  6 ++
 .../20_util/optional/constexpr/124910.cc      | 74 +++++++++++++++++++
 2 files changed, 80 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..49ba7b6b45dd 100644
--- a/libstdc++-v3/include/std/optional
+++ b/libstdc++-v3/include/std/optional
@@ -321,6 +321,12 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
       {
 	_M_engaged = false;
 	_M_payload._M_value.~_Stored_type();
+#if __cpp_lib_optional >= 202106L // full constexpr support
+	if (std::is_constant_evaluated())
+	  // Ensure union _M_payload always has an active member, for sake
+	  // of the core constant expression requirements.
+	  std::construct_at(std::__addressof(_M_payload._M_empty));
+#endif
       }
 
 #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..2f61f7e4e775
--- /dev/null
+++ b/libstdc++-v3/testsuite/20_util/optional/constexpr/124910.cc
@@ -0,0 +1,74 @@
+// { 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;
+  }();
+
+  struct C : std::optional<T> {
+    constexpr C() : std::optional<T>(1) { this->reset(); }
+  };
+  constexpr C x6;
+}
+
+int
+main()
+{
+  do_test<int>();
+  do_test<A>();
+  do_test<B>();
+}
-- 
2.54.0.rc1.54.g60f07c4f5c



More information about the Libstdc++ mailing list