[gcc r16-8748] libstdc++: Fix constantness of engaged -> disengaged std::optional [PR124910]

Patrick Palka ppalka@gcc.gnu.org
Sat Apr 18 23:26:35 GMT 2026


https://gcc.gnu.org/g:f3e63a9fb3b40f81b794b369a1ab616f01e69730

commit r16-8748-gf3e63a9fb3b40f81b794b369a1ab616f01e69730
Author: Patrick Palka <ppalka@redhat.com>
Date:   Sat Apr 18 19:25:59 2026 -0400

    libstdc++: Fix constantness of engaged -> disengaged std::optional [PR124910]
    
    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>

Diff:
---
 libstdc++-v3/include/std/optional                  |  6 ++
 .../testsuite/20_util/optional/constexpr/124910.cc | 74 ++++++++++++++++++++++
 2 files changed, 80 insertions(+)

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>();
+}


More information about the Libstdc++-cvs mailing list