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
>>
>>

Reply via email to