On Mon, May 4, 2026 at 7:41 AM Jakub Jelinek <[email protected]> wrote: > > 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?
I've been going over the P3074R7 paper and it seems to _not_ be intentional. In the paper, Barry mentions that trivial unions should not be allowed to have default member initializers, and that instead a "regular union" should be used. (see section 3.4) So then, do anonymous unions count as "regular unions"? I would believe so, otherwise, the goal of only making previously ill-formed code well-formed fails in the case where there's a default initializer, but all members are trivially destructible. Barry did mention at the end of the section 4 intro of that same paper that the change was "unlikely" to change the meaning of previously well-formed code, so it must definitely have been an oversight. > > 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 >
