https://gcc.gnu.org/g:7ae49df44393ea4ce5024511a3d1b2ba81deca90
commit r17-5007-g7ae49df44393ea4ce5024511a3d1b2ba81deca90 Author: Marek Polacek <[email protected]> Date: Wed Oct 7 15:56:28 2026 -0400 c++: TARGET_EXPR in assignment [PR127710] Here we crash on auto foo = [] {} = {}; on the assert in build_over_call: gcc_assert (TREE_CODE (arg) != TARGET_EXPR || force_elide /* It's from binding the ref parm to a packed field. */ || convs[0]->need_temporary_p || ... ) which makes sure that we are initializing directly from class prvalues rather than copying them. The problem is that we created *&TARGET_EXPR <D.2704, {}> in if (is_really_empty_class (type, /*ignore_vptr*/true)) { /* Avoid copying empty classes, but ensure op= returns an lvalue even if the object argument isn't one. */ to = force_lvalue (to, complain); val = build2 (COMPOUND_EXPR, type, arg, to); suppress_warning (val, OPT_Wunused); } where we want an lvalue, but then in cp_build_addr_expr_1 (Let &* cancel out to simplify resulting code) we get &TARGET_EXPR and then build_over_call strips the & and we end up with a prvalue. The test is actually ([]{}).operator=({}) where binding the []{} object materializes a temporary, operator= returns an lvalue and foo is copy-initialized from that lvalue. My fix is to follow what cp_build_modify_expr (and other places) do: build up a COMPOUND_EXPR and use the TARGET_EXPR_SLOT so that the TARGET_EXPR can't become a prvalue. PR c++/127710 gcc/cp/ChangeLog: * cvt.cc (force_lvalue): For a TARGET_EXPR, build (TARGET_EXPR<...>, slot). gcc/testsuite/ChangeLog: * g++.dg/cpp2a/class-copy1.C: New test. * g++.dg/cpp2a/lambda-assign1.C: New test. Reviewed-by: Jason Merrill <[email protected]> Diff: --- gcc/cp/cvt.cc | 10 +++++++++- gcc/testsuite/g++.dg/cpp2a/class-copy1.C | 29 +++++++++++++++++++++++++++++ gcc/testsuite/g++.dg/cpp2a/lambda-assign1.C | 14 ++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/gcc/cp/cvt.cc b/gcc/cp/cvt.cc index ff54647c5b10..845719c20438 100644 --- a/gcc/cp/cvt.cc +++ b/gcc/cp/cvt.cc @@ -580,7 +580,15 @@ force_rvalue (tree expr, tsubst_flags_t complain) tree force_lvalue (tree expr, tsubst_flags_t complain) { - if (!lvalue_p (expr)) + if (lvalue_p (expr)) + return expr; + + /* If we build up *&TARGET_EXPR, we have an lvalue, but it could be + folded back to a prvalue by removing the *&. */ + if (TREE_CODE (expr) == TARGET_EXPR) + expr = build2 (COMPOUND_EXPR, TREE_TYPE (expr), expr, + TARGET_EXPR_SLOT (expr)); + else { expr = cp_build_addr_expr (expr, complain); expr = cp_build_indirect_ref (input_location, expr, RO_ARROW, complain); diff --git a/gcc/testsuite/g++.dg/cpp2a/class-copy1.C b/gcc/testsuite/g++.dg/cpp2a/class-copy1.C new file mode 100644 index 000000000000..95d7e028f745 --- /dev/null +++ b/gcc/testsuite/g++.dg/cpp2a/class-copy1.C @@ -0,0 +1,29 @@ +// PR c++/127710 +// { dg-do run { target c++20 } } + +struct S { int i; }; +struct A { + A() = default; + A(int i, char c) : i(i), c(c) {} + int i; + char c; +}; +struct B : A { }; +// Try an empty closure as a base. +struct W : decltype([] {}) { int i; }; + +int +main () +{ + S s = S{} = S{1}; + if (s.i != 1) + __builtin_abort (); + + B b = B{} = B{{2, 'x'}}; + if (b.i != 2 || b.c != 'x') + __builtin_abort (); + + W w = W{} = W{{}, 3}; + if (w.i != 3) + __builtin_abort (); +} diff --git a/gcc/testsuite/g++.dg/cpp2a/lambda-assign1.C b/gcc/testsuite/g++.dg/cpp2a/lambda-assign1.C new file mode 100644 index 000000000000..007219a3ba48 --- /dev/null +++ b/gcc/testsuite/g++.dg/cpp2a/lambda-assign1.C @@ -0,0 +1,14 @@ +// PR c++/127710 +// { dg-do compile { target c++20 } } + +auto l1 = [] {} = {}; +void +g () +{ + auto l2 = [] {} = {}; + // [expr.prim.lambda.closure]/17: The closure type associated with + // a lambda-expression [...] has a deleted copy assignment operator + // if the lambda-expression has a lambda-capture. + int i = 9; + auto l3 = [i] {} = {}; // { dg-error "use of deleted function" } +}
