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

Patrick Palka ppalka@redhat.com
Mon Apr 20 21:06:49 GMT 2026


On Mon, 20 Apr 2026, Tomasz Kaminski wrote:

> 
> 
> On Mon, Apr 20, 2026 at 2:44 PM Patrick Palka <ppalka@redhat.com> wrote:
>       On Mon, 20 Apr 2026, Tomasz Kaminski wrote:
> 
>       >
>       >
>       > On Sun, Apr 19, 2026 at 1:28 AM Patrick Palka <ppalka@redhat.com> wrote:
>       >       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.
>       >
>       > I am not aware of this limitation, and I do not see how if fails out of the wording,
>       > The constituent values explicitly exclude inactive union members:
>       > https://eel.is/c++draft/intro.objehttps://docs.google.com/https://docs.google.com/ct#12.2, so empty union simply has none.
> 
>       That term "constituent values" seems to have been introduced in C++26 by
>       P2668R5 and so it's not clear to what extent that wording applies to C++20 :/
> 
> For C++20 we have: https://timsong-cpp.github.io/cppwp/n4861/dcl.constexpr#10
> And there seem be no limitation of union without active members here: 
> https://timsong-cpp.github.io/cppwp/n4861/expr.const#5
> 
>       >
>       > Could you please update the commit description, to reflect actual problem
>       > as described by Jason here:
>       > https://gcc.gnu.org/bugzilla/show_bug.cgi?id=124910#c13
> 
>       [class.union.general]/2 says: In a union, a non-static data member is
>       active if its name refers to an object whose lifetime has begun and has
>       not ended.  How does destroying an active member not make it inactive?
> 
> I think the point of comment, is that this change is a workaround for GCC bug
> (the _M_value member is still considered active member, but it shouldn't).

I see, that makes sense.  So Clang and GCC happened to have (more or less)
the same bug!  With Jason's fix r16-8767 from today it appears my
<optional> change is no longer necessary for GCC, but I suppose we still
want it for Clang?

> 
> 
>       >
>       >  
>       >
>       >       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