On Fri, Oct 09, 2026 at 03:43:36PM +0200, Tomasz Kaminski wrote:
> Could you add some guaranteed copy elision cases. To be able to crete
> uninitialized prvalue we need some cosntructor doing so
> struct X { constexpr X(int p) : x(p) {}; int x; int y };
> X create() { return X(10); }
> void pass(X);
> 
> void test() {
>   X x = X(10); // OK, no copy
>   pass(X(10)); // OK, no copy
>   X x2 = create(); // OK, no copy
>   pass(create()); // OK, no copy
> 
>   X x3 = x; // FAIL, copy
>   pass(x); // FAIL, copy
> }
> 
> constexpr X gx = create(); // FAILs,
> // due constexpr variable with uninitialized consitiutent value,
> // but initialization should be OK

That means I should probably check for the elision cases in
all 3 places rather than just one.  Incremental patch for that
below.  The above testcase doesn't compile for various reasons,
if I change it to:
struct X { constexpr X (int p) : x(p) {}; int x; int y; };
constexpr X create () { return X (10); }
constexpr void pass (X) {}

constexpr bool
test (int z)
{
  X x = X (10); // OK, no copy
  pass (X (10)); // OK, no copy
  X x2 = create (); // OK, no copy
  pass (create ()); // OK, no copy

  if (z == 1)
    X x3 = x; // FAIL, copy
  else if (z == 2)
    pass (x); // FAIL, copy
  return true;
}

static_assert (test (0));
constexpr bool a = test (1);
constexpr bool b = test (2);
constexpr X gx = create ();
then even with the patch below it doesn't work.  If I s/constexpr //
and remove the static_assert, I see in gimple dump (and at -O0 optimized
dump too):
      X::X (&x, 10);
great, copy elided in the first case.
          X::X (&D.2725, 10);
          pass (D.2725);
pass (X (10)); case, object constructed in a temporary, passed by value.
Shouldn't we reject that at constexpr time?
      x2 = create ();
This isn't a non-POD which is returned by invisible reference, so there is
copying of the result involved as well.
      D.2735 = create ();
      pass (D.2735);
This does even 2 copies.

So, from this I'd say (unless C++ requires copy elision even with
implicitly-defined copy ctor and assignment op, perhaps we should reject
it in all but the X x = X (10); cases here?

Of course, the test should be extended to same thing with say non-trivial
constexpr destructor.  In that case (again, with everything non-constexpr
and looking at dumps), even the pass (X (10)); case has copying elided:
              X::X (&D.2717, 10);
...
                  pass (&D.2717);
the x2 case looks identical on the caller side but is actually returning
by hidden reference, so no copying either:
          x2 = create (); [return slot optimization]
with
  X::X (<retval>, 10);
  return <retval>;
in create () and finally the pass (create ()); also elided:
                  D.2720 = create (); [return slot optimization]
...
                      pass (&D.2720);
That said, clang++ -std=c++20 seems to accept/reject the above testcase
as you expected in there, and with constexpr ~X () {} added likewise.
So, if I'd should be following that, I guess I'd need to treat e.g. the
caller side of aggregate returning as something to never diagnose (with the
assumption that the callee reports it already).

--- a/gcc/cp/constexpr.cc       2026-10-09 13:14:20.702113761 +0200
+++ b/gcc/cp/constexpr.cc       2026-10-09 16:19:24.771075854 +0200
@@ -3473,6 +3473,24 @@ check_uninit_subobjects (const constexpr
   return true;
 }
 
+/* Return true if copy from FROM will be elided by cp_gimplify_init_expr.  */
+
+static bool
+copy_elided_p (tree from)
+{
+  if (TREE_CODE (from) == TARGET_EXPR
+      && TARGET_EXPR_INITIAL (from)
+      && (TARGET_EXPR_ELIDING_P (from)
+         || !TREE_ADDRESSABLE (TREE_TYPE (from))))
+    from = TARGET_EXPR_INITIAL (from);
+  while (TREE_CODE (from) == COMPOUND_EXPR)
+    from = TREE_OPERAND (from, 1);
+  if (TREE_CODE (from) == AGGR_INIT_EXPR
+      || TREE_CODE (from) == VEC_INIT_EXPR)
+    return true;
+  return false;
+}
+
 /* Subroutine of cxx_eval_call_expression.
    We are processing a call expression (either CALL_EXPR or
    AGGR_INIT_EXPR) in the context of CTX.  Evaluate
@@ -3591,7 +3609,8 @@ cxx_bind_parameters_in_call (const const
 
       if (cxx_dialect >= cxx20
          && TREE_CODE (arg) == CONSTRUCTOR
-         && !*non_constant_p)
+         && !*non_constant_p
+         && !copy_elided_p (x))
        {
          location_t arg_loc = cp_expr_loc_or_input_loc (x);
          if (check_uninit_subobjects (ctx, arg_loc, arg))
@@ -9187,28 +9206,12 @@ cxx_eval_store_expression (const constex
       && !TREE_CLOBBER_P (init)
       && TREE_CODE (t) != MODOP_EXPR
       && (TREE_CODE (t) != INIT_EXPR
-         || !INIT_EXPR_UNION_LIFETIME_START_P (t)))
+         || (!INIT_EXPR_UNION_LIFETIME_START_P (t)
+             /* Don't diagnose if the copy will be elided.  */
+             && !copy_elided_p (TREE_OPERAND (t, 1)))))
     {
       location_t init_loc = cp_expr_loc_or_input_loc (TREE_OPERAND (t, 1));
-      bool check = true;
-      if (TREE_CODE (t) == INIT_EXPR)
-       {
-         /* For INIT_EXPR, don't diagnose copying of aggregates with
-            indeterminate subobjects if the copying will be elided
-            in cp_gimplify_init_expr.  */
-         tree from = TREE_OPERAND (t, 1);
-         if (TREE_CODE (from) == TARGET_EXPR
-             && TARGET_EXPR_INITIAL (from)
-             && (TARGET_EXPR_ELIDING_P (from)
-                 || !TREE_ADDRESSABLE (TREE_TYPE (from))))
-           from = TARGET_EXPR_INITIAL (from);
-         while (TREE_CODE (from) == COMPOUND_EXPR)
-           from = TREE_OPERAND (from, 1);
-         if (TREE_CODE (from) == AGGR_INIT_EXPR
-             || TREE_CODE (from) == VEC_INIT_EXPR)
-           check = false;
-       }
-      if (check && check_uninit_subobjects (ctx, init_loc, init))
+      if (check_uninit_subobjects (ctx, init_loc, init))
        {
          *non_constant_p = true;
          return t;
@@ -10328,7 +10331,8 @@ cxx_eval_constant_expression (const cons
              return NULL_TREE;
            if (cxx_dialect >= cxx20
                && TREE_CODE (init) == CONSTRUCTOR
-               && !*non_constant_p)
+               && !*non_constant_p
+               && !copy_elided_p (DECL_INITIAL (r)))
              {
                location_t init_loc
                  = cp_expr_loc_or_input_loc (DECL_INITIAL (r));


        Jakub

Reply via email to