[committed v2] libstdc++: Fix constantness of engaged -> disengaged std::optional [PR124910]
Tomasz Kaminski
tkaminsk@redhat.com
Mon Apr 20 07:26:55 GMT 2026
On Mon, Apr 20, 2026 at 9:25 AM Tomasz Kaminski <tkaminsk@redhat.com> 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.object#12.2, so empty union simply has none.
>
> 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
>
Ah sorry, I haven't noticed that the patch is already committed.
>
>
>
>>
>> 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
>>
>>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://gcc.gnu.org/pipermail/libstdc++/attachments/20260420/48e8adb6/attachment.htm>
More information about the Libstdc++
mailing list