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

Reply via email to