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
>

Reply via email to