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. > >> >> 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. > In particular there are zero copies of T in following example, assuming `__builin_bitcast` produces prvalue template<typename T, typename U> T bit_cast(U const& u) { return __builtin_bitcast(T, u) ; } T x = bit_cast<T>(u); > > >> >> 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 >> >>
