[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