On Fri, Oct 9, 2026 at 5:27 PM Tomasz Kaminski <[email protected]> wrote:
> > > 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. > Implementations can, of course make these copies because they are not observable for trivially copyable types by the program, other than cases like this. We need to pretend that they do not happen. > >> 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 >> >>
