On Fri, Oct 9, 2026 at 4:55 PM Jakub Jelinek <[email protected]> wrote:
> 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.
In such cases, the standard says that X(10) directly constructs the
parameter.
> 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.
>
And zero copies, per the standard.
>
> 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?
>
Yes, C++ standard does not copy objects in all above cases, when the source
is prvalue the destination is directly initialized from it. Regardless of
copy-constructor
(the copy/move cosntructor can be deleted and above is required to work).
See: https://godbolt.org/z/181ej3sfE
So I think if possible we should avoid whole "copy" of intermiediate value,
if source
is prvalue, if that is something you can express.
>
> 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
>
>