On Wed, Feb 11, 2026 at 02:56:36AM +0200, Ville Voutilainen wrote: > > Anyway, with the hope that CWG 3123 is voted in, this patch adds > > a testcase for it. > > This was discussed some moments ago on the Core reflector, and it was > pointed out that it's quite > intentional in the proposed resolution of CWG 3123 that there's no > overload resolution used for > determining whether iteration is the chosen approach instead of > destructuring, just a viability check. > As far as I understand the code, finish_call_expr indeed does overload > resolution. > > This additional testcase was provided: https://godbolt.org/z/o39hEs876 > > In it, overload resolution fails due to an ambiguity, for iteration, > and then destructuring is used instead. But the intent > of the proposed resolution is that the semantics are (more) similar to > what range-for uses, so the intent > is that for iteration, only the presence of viable overloads is > checked, and then iteration is committed to, > and then the testcase should be rejected because the actual attempt to > expand with iteration fails due to the aforementioned > ambiguity. > > None of that affects this patch as such, but I think it's worth > pointing out where this should eventually be going.
Ah, you're right. We don't have any API to return whether there are any viable candidates. I was looking into adding one, but realized that finish_call_expr does way too many things before calling build_new_function_call plus in some cases it calls build_op_call which does further stuff that would also need to be duplicated before we can reach to splice_viable. So, instead of duplicating all of that I've added tf_any_viable (using an unused bit in tsubst_flags_t), which is then handled in perform_overload_resolution, build_new_function_call and build_op_call functions to return void_node instead of actually building any calls if there are any viable candidates (and keep returning error_mark_node if something earlier fails or there are no viable candidates). The new testcase adds some extra new tests, e.g. struct with begin and end non-static data members (my reading is that in that case E.begin and E.end has been found, so we commit to iterating expansion stmt and fail unless e.g. they whould have class type and usable operator ()). So far tested with GXX_TESTSUITE_STDS=98,11,14,17,20,23,26 make -j32 -k check-g++ RUNTESTFLAGS="dg.exp='reflect/* expansion-stmt*'" 2026-02-12 Jakub Jelinek <[email protected]> * cp-tree.h: Implement proposed CWG 3123 resolution. (enum tsubst_flags): Add tf_any_viable enumerator. Indent all comments the same. * call.cc (perform_overload_resolution): For tf_any_viable return the first candidate instead of doing tourney. (build_new_function_call): For tf_any_viable return void_node if any viable candidates are found rather than build_over_call. (build_op_call): FOr tf_any_viable return void_node if any viable candidates are found instead of calling tourney etc. * parser.cc (cp_perform_range_for_lookup): Pass COMPLAIN to cp_range_for_member_function calls. If not tf_error, for successful ADL if one or both of finish_call_expr calls returns error_mark_node, retry with tf_any_viable. If both begin and end expressions have a viable candidate or when member lookup found both begin and end members, return NULL_TREE rather than error_mark_node even when both *begin and *end are error_mark_node. (cp_range_for_member_function): Add COMPLAIN argument, pass it down to finish_class_member_access_expr and finish_call_expr. * pt.cc (finish_expansion_stmt): Use esk_iterating if cp_perform_range_for_lookup returned something other than error_mark_node or if both begin_expr and end_expr are not error_mark_node. * g++.dg/cpp26/expansion-stmt29.C: New test. * g++.dg/cpp26/expansion-stmt30.C: New test. --- gcc/cp/cp-tree.h.jj 2026-02-12 10:32:24.644331171 +0100 +++ gcc/cp/cp-tree.h 2026-02-12 10:37:18.954376200 +0100 @@ -6107,20 +6107,22 @@ enum tsubst_flags { for calls in decltype (5.2.2/11). */ tf_partial = 1 << 8, /* Doing initial explicit argument substitution in fn_type_unification. */ - tf_fndecl_type = 1 << 9, /* Substituting the type of a function - declaration. */ - tf_no_cleanup = 1 << 10, /* Do not build a cleanup - (build_target_expr and friends) */ - /* 1 << 11 is available. */ + tf_fndecl_type = 1 << 9, /* Substituting the type of a function + declaration. */ + tf_no_cleanup = 1 << 10, /* Do not build a cleanup + (build_target_expr and friends) */ + tf_any_viable = 1 << 11, /* Return void_node if there are any viable + candidates. */ tf_tst_ok = 1 << 12, /* Allow a typename-specifier to name a template (C++17 or later). */ - tf_dguide = 1 << 13, /* Building a deduction guide from a ctor. */ + tf_dguide = 1 << 13, /* Building a deduction guide from a ctor. */ tf_qualifying_scope = 1 << 14, /* Substituting the LHS of the :: operator. Affects TYPENAME_TYPE resolution from make_typename_type. */ - tf_no_name_lookup = 1 << 15, /* Don't look up the terminal name of an - outermost id-expression, or resolve its - constituent template-ids or qualified-ids. */ + tf_no_name_lookup = 1 << 15, /* Don't look up the terminal name of an + outermost id-expression, or resolve its + constituent template-ids or + qualified-ids. */ /* Convenient substitution flags combinations. */ tf_warning_or_error = tf_warning | tf_error }; --- gcc/cp/call.cc.jj 2026-02-06 11:18:47.075640238 +0100 +++ gcc/cp/call.cc 2026-02-12 11:30:08.203023552 +0100 @@ -5190,7 +5190,12 @@ perform_overload_resolution (tree fn, *candidates = splice_viable (*candidates, false, any_viable_p); if (*any_viable_p) - cand = tourney (*candidates, complain); + { + if (complain & tf_any_viable) + cand = *candidates; + else + cand = tourney (*candidates, complain); + } else cand = NULL; @@ -5305,10 +5310,10 @@ build_new_function_call (tree fn, vec<tr } result = error_mark_node; } + else if (complain & tf_any_viable) + return void_node; else - { - result = build_over_call (cand, LOOKUP_NORMAL, complain); - } + result = build_over_call (cand, LOOKUP_NORMAL, complain); if (flag_coroutines && result @@ -5593,6 +5598,8 @@ build_op_call (tree obj, vec<tree, va_gc } else { + if (complain & tf_any_viable) + return void_node; cand = tourney (candidates, complain); if (cand == 0) { --- gcc/cp/parser.cc.jj 2026-02-10 15:29:56.686966880 +0100 +++ gcc/cp/parser.cc 2026-02-12 11:11:34.941766559 +0100 @@ -2642,7 +2642,7 @@ static tree cp_parser_range_for static void do_range_for_auto_deduction (tree, tree, cp_decomp *, bool); static tree cp_range_for_member_function - (tree, tree); + (tree, tree, tsubst_flags_t); static tree cp_parser_expansion_statement (cp_parser *, bool *); static tree cp_parser_jump_statement @@ -16245,8 +16245,8 @@ cp_perform_range_for_lookup (tree range, if (member_begin != NULL_TREE && member_end != NULL_TREE) { /* Use the member functions. */ - *begin = cp_range_for_member_function (range, id_begin); - *end = cp_range_for_member_function (range, id_end); + *begin = cp_range_for_member_function (range, id_begin, complain); + *end = cp_range_for_member_function (range, id_end, complain); } else { @@ -16255,14 +16255,12 @@ cp_perform_range_for_lookup (tree range, vec_safe_push (vec, range); - member_begin = perform_koenig_lookup (id_begin, vec, - complain); + member_begin = perform_koenig_lookup (id_begin, vec, complain); if ((complain & tf_error) == 0 && member_begin == id_begin) return error_mark_node; *begin = finish_call_expr (member_begin, &vec, false, true, complain); - member_end = perform_koenig_lookup (id_end, vec, - tf_warning_or_error); + member_end = perform_koenig_lookup (id_end, vec, complain); if ((complain & tf_error) == 0 && member_end == id_end) { *begin = error_mark_node; @@ -16270,6 +16268,29 @@ cp_perform_range_for_lookup (tree range, } *end = finish_call_expr (member_end, &vec, false, true, complain); + if ((complain & tf_error) == 0 + && (*begin == error_mark_node || *end == error_mark_node)) + { + /* Expansion stmt should be iterating if there are any + viable candidates for begin and end. If both finish_call_expr + with tf_none succeeded, there certainly are, if not, + retry with tf_any_viable to check if there were any viable + candidates. */ + if (*begin == error_mark_node + && finish_call_expr (member_begin, &vec, false, true, + tf_any_viable) == error_mark_node) + { + *end = error_mark_node; + return error_mark_node; + } + if (*end == error_mark_node + && finish_call_expr (member_end, &vec, false, true, + tf_any_viable) == error_mark_node) + { + *begin = error_mark_node; + return error_mark_node; + } + } } /* Last common checks. */ @@ -16277,6 +16298,13 @@ cp_perform_range_for_lookup (tree range, { /* If one of the expressions is an error do no more checks. */ *begin = *end = error_mark_node; + /* But signal to finish_expansion_stmt whether this is + destructuring (error_mark_node returned) or iterating + (something else returned). If we got here, range.begin and + range.end members were found or begin (range) and end (range) + found any viable candidates. */ + if ((complain & tf_error) == 0) + return NULL_TREE; return error_mark_node; } else if (type_dependent_expression_p (*begin) @@ -16314,20 +16342,20 @@ cp_perform_range_for_lookup (tree range, Builds a tree for RANGE.IDENTIFIER(). */ static tree -cp_range_for_member_function (tree range, tree identifier) +cp_range_for_member_function (tree range, tree identifier, + tsubst_flags_t complain) { tree member, res; member = finish_class_member_access_expr (range, identifier, - false, tf_warning_or_error); + false, complain); if (member == error_mark_node) return error_mark_node; releasing_vec vec; res = finish_call_expr (member, &vec, /*disallow_virtual=*/false, - /*koenig_p=*/false, - tf_warning_or_error); + /*koenig_p=*/false, complain); return res; } --- gcc/cp/pt.cc.jj 2026-02-11 11:21:46.434567063 +0100 +++ gcc/cp/pt.cc 2026-02-12 11:56:20.415553822 +0100 @@ -33197,11 +33197,9 @@ finish_expansion_stmt (tree expansion_st range_temp = convert_from_reference (build_range_temp (expansion_init)); iter_type = cp_perform_range_for_lookup (range_temp, &begin_expr, &end_expr, tf_none); - if (begin_expr != error_mark_node && end_expr != error_mark_node) - { - kind = esk_iterating; - gcc_assert (iter_type); - } + if (iter_type != error_mark_node + || (begin_expr != error_mark_node && (end_expr != error_mark_node))) + kind = esk_iterating; } if (kind == esk_iterating) { --- gcc/testsuite/g++.dg/cpp26/expansion-stmt29.C.jj 2026-02-12 11:13:28.588853229 +0100 +++ gcc/testsuite/g++.dg/cpp26/expansion-stmt29.C 2026-02-12 11:13:28.588853229 +0100 @@ -0,0 +1,15 @@ +// CWG 3123 +// { dg-do run { target c++26 } } + +#include <tuple> + +int +main () +{ + long l = 0; + std::tuple <int, long, unsigned> t = { 1, 2L, 3U }; + template for (auto &&x : t) + l += x; + if (l != 6L) + __builtin_abort (); +} --- gcc/testsuite/g++.dg/cpp26/expansion-stmt30.C.jj 2026-02-12 11:19:40.674588891 +0100 +++ gcc/testsuite/g++.dg/cpp26/expansion-stmt30.C 2026-02-12 11:41:31.460519574 +0100 @@ -0,0 +1,38 @@ +// CWG 3123 +// { dg-do run { target c++26 } } + +namespace M { + struct B {}; + + template <typename T> + auto *begin (T &t) { return &t.array[0]; } +} + +namespace N { + struct S : M::B { int array[8]; }; + + template <typename T> + auto *begin (T &s) { return &s.array[0]; } + + auto *end (const S &s) { return &s.array[8]; } +} + +struct V { int begin, end; }; + +namespace O { + struct B { int b; }; + struct C { int operator () (int) { return 42; } } begin, end; +} + +void +foo () +{ + for (auto i : N::S {}) // { dg-error "call of overloaded 'begin\\\(N::S\\\&\\\)' is ambiguous" } + ; + template for (auto i : N::S {}) + ; // { dg-error "call of overloaded 'begin\\\(const N::S\\\&\\\)' is ambiguous" } + template for (auto i : V {}) + ; // { dg-error "cannot be used as a function" } + template for (auto i : O::B {}) + ; +} Jakub
