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

Patrick Palka ppalka@redhat.com
Sat Apr 18 02:25:50 GMT 2026


On Fri, 17 Apr 2026, Patrick Palka wrote:

> On Fri, 17 Apr 2026, Patrick Palka wrote:
> 
> > 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));
> 
> Whoops, forgot to test with GLIBCXX_TESTSUITE_STDS that includes 17.
> construct_at is C++20-only, and so is full constexpr optional support.
> So we should guard this added code with __cpp_lib_optional >= 202106L
> like so.  Retesting thoroughly overnight...

In C++17 optional was only partially constexpr, supporting only simple
construction and assignment, and I don't think we can end up with an
optional in such a state using only those operations.  So it should be
fine to use construct_at and restrict ourselves to C++20, I think.

> 
> -- >8 --
> 
> 	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, call construct_at to
> 	activate _M_empty.
> 	* testsuite/20_util/optional/constexpr/124910.cc: New test.
> ---
>  libstdc++-v3/include/std/optional             |  4 ++
>  .../20_util/optional/constexpr/124910.cc      | 63 +++++++++++++++++++
>  2 files changed, 67 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..8370fac08109 100644
> --- a/libstdc++-v3/include/std/optional
> +++ b/libstdc++-v3/include/std/optional
> @@ -321,6 +321,10 @@ _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())
> +	  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..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