[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