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