Hi! So, I've started working on the P3074R7 trivial unions paper except for that [class.default.ctor]/4 long sentence that P3726R2 removed again. I hope I'm missing something, but the more I look into this the more problems I see.
The paper contains: "which wasn’t particularly surprising since this change is entirely about making existing ill-formed code valid" but that doesn't seem to be the case to me, see below. One small nit first, I don't understand e.g. the https://eel.is/c++draft/class.default.ctor#2.2 addition of "X is a non-union class and", because the bullet only talks about non-variant non-static data members, aren't all non-static data members in a union already variant members and so the addition doesn't change anything? More importantly, defaulted dtor would be previously deleted if it had some variant subobject with deleted or inaccessible or non-trivial dtor, my understanding is that the paper wants to intentionally limit that to variant members with default initializer, but https://eel.is/c++draft/class.mem#class.dtor-7.2.2 doesn't seem to do that. For one, it doesn't apply to variant members in anonymous unions because it only talks about union classes, so I believe struct A { A (int); ~A () = delete; int a; }; struct B { union { A a = 42; int b; }; }; B::~B () per the new rules isn't deleted. And for unions, guess whether the subobject with default initializer and deleted dtor is now covered by https://eel.is/c++draft/class.mem#class.dtor-7.2.1 because in that case the ctor due to https://eel.is/c++draft/class.default.ctor#2.4 will be deleted, but that checks for accessibility of the subobject dtor in the default ctor, not in the dtor. Most importantly, the https://eel.is/c++draft/class.mem#class.dtor-7.2.1 new rule doesn't seem to match the "entirely about making existing ill-formed code valid" comment, I believe union C { int a = 42; long b; } u; used to be valid in C++11 to C++23, but in my understanding is now invalid. There is a variant member, so https://eel.is/c++draft/class.default.ctor#3.2 says that C::C () is non-trivial, and then https://eel.is/c++draft/class.mem#class.dtor-7.2.1 says that if the default ctor is non-trivial, defaulted C::~C () is defined as deleted. Was that intentional? The patch below drops the default_init_uninitialized_part check in all variant members because already C++11 had there the non-variant case, the second hunk doesn't implement the paper exactly as written (see the more importantly point above), as it doesn't differentiate between the sfk_destructor case due to dtor_from_ctor (that would look like what it writes right now) but if !dtor_from_ctor with the current wording it would need to limit itself just to the toplevel unions (not anonymous unions) and somehow check if the corresponding subobj dtor is non-trivial without checking if it is deleted or doesn't exist (how?). The synthesized_method_walk hunk I believe implements https://eel.is/c++draft/class.mem#class.dtor-7.2.1 as written with the above mentioned implications. Regarding P3726R2, guess we want some type generic builtin (__builtin_start_lifetime ?) which will be used in std::start_lifetime template and at constant evaluation time will set/switch active union member (what should happen if some other union member is active and there is non-trivial dtor?). Plus some tweaks on what is valid constant expression for C++26 in (possibly multi-dimensional) arrays inside of unions. --- gcc/c-family/c-cppbuiltin.cc.jj 2026-04-22 15:03:36.485207851 +0200 +++ gcc/c-family/c-cppbuiltin.cc 2026-05-04 11:13:26.546729223 +0200 @@ -1122,6 +1122,7 @@ c_cpp_builtins (cpp_reader *pfile) cpp_define (pfile, "__cpp_impl_reflection=202603L"); else cpp_warn (pfile, "__cpp_impl_reflection"); + cpp_define (pfile, "__cpp_trivial_union=202603L"); } if (flag_concepts && cxx_dialect > cxx14) cpp_define (pfile, "__cpp_concepts=202002L"); --- gcc/cp/method.cc.jj 2026-05-04 09:08:58.427717395 +0200 +++ gcc/cp/method.cc 2026-05-04 12:33:01.324557261 +0200 @@ -2771,6 +2771,7 @@ walk_field_subobs (tree fields, special_ bad = false; if (CP_TYPE_CONST_P (mem_type) + && TREE_CODE (ctx) != UNION_TYPE && default_init_uninitialized_part (mem_type)) { if (diag) @@ -2847,6 +2848,15 @@ walk_field_subobs (tree fields, special_ else argtype = NULL_TREE; + if (cxx_dialect >= cxx26 && TREE_CODE (ctx) == UNION_TYPE) + { + if (sfk == sfk_constructor || sfk == sfk_inheriting_constructor) + continue; + + if (sfk == sfk_destructor && DECL_INITIAL (field) == NULL_TREE) + continue; + } + rval = locate_fn_flags (mem_type, fnname, argtype, flags, complain); process_subob_fn (rval, sfk, spec_p, trivial_p, deleted_p, @@ -2985,6 +2995,35 @@ synthesized_method_walk (tree ctype, spe /* The synthesized method will call base dtors, but check complete here to avoid having to deal with VTT. */ fnname = complete_dtor_identifier; + + if (TREE_CODE (ctype) == UNION_TYPE + && cxx_dialect >= cxx26 + && deleted_p) + { + /* [class.dtor]/(7.2.1): + A defaulted destructor for a class X is defined as deleted if X + is a union and + -- overload resolution to select a constructor to + default-initialize an object of type X either fails or selects + a constructor that is either deleted or not trivial. */ + tree ctor = locate_ctor (ctype); + if (ctor == NULL_TREE) + { + *deleted_p = true; + if (diag) + inform (DECL_SOURCE_LOCATION (TYPE_NAME (ctype)), + "default constructor of %qT unusable", ctype); + return; + } + else if (!trivial_fn_p (ctor)) + { + *deleted_p = true; + if (diag) + inform (DECL_SOURCE_LOCATION (TYPE_NAME (ctype)), + "default constructor of %qT is not trivial", ctype); + return; + } + } } else if (SFK_ASSIGN_P (sfk)) fnname = assign_op_identifier; --- gcc/testsuite/g++.dg/DRs/dr2581-1.C.jj 2026-03-27 10:17:15.320311386 +0100 +++ gcc/testsuite/g++.dg/DRs/dr2581-1.C 2026-05-04 11:15:02.107141015 +0200 @@ -94,7 +94,7 @@ #undef __cpp_template_parameters #undef __cpp_template_template_args // { dg-warning "undefining '__cpp_template_template_args'" "" { target c++20 } } #undef __cpp_threadsafe_static_init // { dg-warning "undefining '__cpp_threadsafe_static_init'" "" { target c++20 } } -#undef __cpp_trivial_union +#undef __cpp_trivial_union // { dg-warning "undefining '__cpp_trivial_union'" "" { target c++26 } } #undef __cpp_unicode_characters // { dg-warning "undefining '__cpp_unicode_characters'" "" { target c++20 } } #undef __cpp_unicode_literals // { dg-warning "undefining '__cpp_unicode_literals'" "" { target c++20 } } #undef __cpp_user_defined_literals // { dg-warning "undefining '__cpp_user_defined_literals'" "" { target c++20 } } --- gcc/testsuite/g++.dg/DRs/dr2581-2.C.jj 2026-04-22 15:03:36.485585053 +0200 +++ gcc/testsuite/g++.dg/DRs/dr2581-2.C 2026-05-04 11:15:29.961678074 +0200 @@ -95,7 +95,7 @@ #define __cpp_template_parameters 202502L #define __cpp_template_template_args 201611L // { dg-error "'__cpp_template_template_args' redefined" "" { target c++20 } } #define __cpp_threadsafe_static_init 200806L // { dg-error "'__cpp_threadsafe_static_init' redefined" "" { target c++20 } } -#define __cpp_trivial_union 202502L +#define __cpp_trivial_union 202603L // { dg-error "'__cpp_trivial_union' redefined" "" { target c++26 } } #define __cpp_unicode_characters 200704L // { dg-error "'__cpp_unicode_characters' redefined" "" { target c++17 } } #define __cpp_unicode_literals 200710L // { dg-error "'__cpp_unicode_literals' redefined" "" { target c++20 } } #define __cpp_user_defined_literals 200809L // { dg-error "'__cpp_user_defined_literals' redefined" "" { target c++20 } } --- gcc/testsuite/g++.dg/cpp26/feat-cxx26.C.jj 2026-03-27 10:17:15.654305935 +0100 +++ gcc/testsuite/g++.dg/cpp26/feat-cxx26.C 2026-05-04 11:14:08.680028970 +0200 @@ -652,3 +652,9 @@ #elif __cpp_expansion_statements != 202506 # error "__cpp_expansion_statements != 202506" #endif + +#ifndef __cpp_trivial_union +# error "__cpp_trivial_union" +#elif __cpp_trivial_union != 202603 +# error "__cpp_trivial_union != 202603" +#endif --- gcc/testsuite/g++.dg/cpp26/trivial-union1.C.jj 2026-05-04 11:17:47.404393790 +0200 +++ gcc/testsuite/g++.dg/cpp26/trivial-union1.C 2026-05-04 12:43:42.852056550 +0200 @@ -0,0 +1,37 @@ +// P3074R7 - trivial unions (was std::uninitialized<T>) +// P3726R2 - Adjustments to Union Lifetime Rules +// { dg-do compile { target c++11 } } + +#include <type_traits> + +// These two were incorrectly deleted, except for C++26 +// where the dtor is deleted. +union A { int a; const int b; }; +static_assert (std::is_default_constructible <A>::value, ""); +static_assert (std::is_trivially_default_constructible <A>::value, ""); +struct B { int a; union { int b; const int c; }; }; +static_assert (std::is_default_constructible <B>::value, ""); +static_assert (std::is_trivially_default_constructible <B>::value, ""); +// C::C() is incorrectly not deleted in C++11 to 23, but in C++26 it should +// not be deleted. +union C { const int a = 42; const long b; ~C (); }; +#if __cpp_trivial_union >= 202502L +static_assert (std::is_default_constructible <C>::value, ""); +static_assert (!std::is_trivially_default_constructible <C>::value, ""); +#endif +struct D { D () = delete; D (int); ~D () = default; }; +union E { D a = 42; D b; ~E (); }; +static_assert (std::is_default_constructible <E>::value, ""); +struct F { int a; union { D b = 42; D c; }; }; +static_assert (std::is_default_constructible <F>::value, ""); +struct G { G (); ~G (); }; +struct H { H (int); ~H (); int h; protected: H () = default; }; +union I { int a; const int b; ~I (); }; +static_assert (std::is_default_constructible <I>::value, ""); +static_assert (!std::is_trivially_default_constructible <I>::value, ""); +union J { D a; int b; }; +#if __cpp_trivial_union >= 202502L +static_assert (std::is_default_constructible <J>::value, ""); +#else +static_assert (!std::is_default_constructible <J>::value, ""); +#endif Jakub
