On 9/4/26 5:56 PM, Vladislav Semykin wrote:
- Jason: lambda_expr_this_capture was missing the in_typeid_probe deferral that process_outer_var_ref has for 'this'.
Looking at the change, I think I was wrong about this bit and what was there already worked fine; we can just use cp_unevaluated_operand because we don't need to worry about VLAs.
For future reference, the "Changes since v3" and testing info should go in the email body rather than the commit message.
With those tweaks, here's what I'm pushing. Thanks! Jason
From 332497e1737010463b474066537be34918d5952f Mon Sep 17 00:00:00 2001 From: Vladislav Semykin <[email protected]> Date: Sat, 5 Sep 2026 00:19:35 +0300 Subject: [PATCH] c++: fix typeid capture in generic lambdas [PR c++/127132] To: [email protected] [expr.prim.lambda.capture]/7 (P0588R1 / CWG1468) requires a lambda to capture a local entity if the expression would be potentially evaluated were the effect of any enclosing typeid expressions ignored. Capture must not depend on whether the typeid operand is itself evaluated. The PR125886 two-pass parse gated the capture-triggering evaluated re-parse on processing_template_decl == 0. A generic lambda's own auto parameter makes that nonzero regardless of the operand, so capture of an outer polymorphic glvalue was skipped and the resulting uncaptured decl ICEd at RTL expand (PR127132). Do not teach typeid_evaluated_p to treat a template context as evaluated: mark_used must still suppress instantiation while the operand is genuinely unevaluated (declval<T>()), and resolves_to_fixed_type_p already returns "fixed" whenever in_template_context. Instead, add cp_unevaluated_typeid_cutoff: process_outer_var_ref and lambda_expr_this_capture suppress capture only when cp_unevaluated_operand exceeds that cutoff. A typeid probe passes typeid_operand=true to cp_unevaluated, which raises the cutoff only when operand == cutoff at probe start, so sizeof (typeid (b)) stays suppressed while typeid (typeid (x)) looks through every enclosing typeid. Inside the cutoff, capture is attempted as usual, but a missing capture-default is not diagnosed there - the real evaluated-ness isn't known yet, and a later, non-probe pass will diagnose it if the operand turns out evaluated. Outside a lambda (a local class, a default argument) the capture attempt has nothing to attach to and falls through unchanged, so params() in pr125886.C still sees an unevaluated operand with no extra special-casing. Every force-reset site that clears cp_unevaluated_operand also save/reset/restores the cutoff, including cp_evaluated. processing_template_decl == 0 turned out to still be the right gate for the evaluated re-parse itself: mark_used and build_typeid both already no-op unconditionally in any template context, independent of which pass hits them, so the two-pass parse needs no extra gate there either. gcc/cp/ChangeLog: PR c++/127132 * cp-tree.h (saved_scope): Save unevaluated_typeid_cutoff. (cp_unevaluated_typeid_cutoff): Declare. (cp_unevaluated): Add a defaulted TYPEID_OPERAND parameter; save and restore the cutoff. (cp_evaluated): Save, reset, and restore the cutoff. * parser.cc (cp_unevaluated_typeid_cutoff): Define. (cp_unevaluated::cp_unevaluated, cp_unevaluated::~cp_unevaluated): Implement TYPEID_OPERAND cutoff handling. (cp_parser_postfix_expression): Pass typeid_operand=true for the typeid probe. * pt.cc (instantiate_class_template): Save/reset/restore the cutoff around NSDMI / lambda capture substitution. (tsubst_expr): Pass typeid_operand=true for TYPEID_EXPR's probe. * semantics.cc (process_outer_var_ref): Allow lambda capture through a typeid probe per [expr.prim.lambda.capture]/7; defer the "not captured" diagnostic to a later, non-probe pass. * lambda.cc (lambda_expr_this_capture): Same cutoff and deferral for 'this'. * constraint.cc (tsubst_constraint_variables): Save/reset/restore the cutoff with cp_unevaluated_operand. * name-lookup.cc (push_to_top_level, pop_from_top_level): Same. (local_state_t): Same. gcc/testsuite/ChangeLog: PR c++/127132 * g++.dg/rtti/typeid-lambda-capture-pr127132.C: New test, consolidating the original PR127132 reproducer with the /7 cases previously added to pr125886.C, plus this-capture coverage and closure-size checks. Signed-off-by: Vladislav Semykin <[email protected]> Reviewed-by: Jason Merrill <[email protected]> --- gcc/cp/cp-tree.h | 29 ++- gcc/cp/constraint.cc | 3 + gcc/cp/lambda.cc | 6 +- gcc/cp/name-lookup.cc | 7 + gcc/cp/parser.cc | 15 +- gcc/cp/pt.cc | 5 +- gcc/cp/semantics.cc | 14 +- .../rtti/typeid-lambda-capture-pr127132.C | 169 ++++++++++++++++++ 8 files changed, 237 insertions(+), 11 deletions(-) create mode 100644 gcc/testsuite/g++.dg/rtti/typeid-lambda-capture-pr127132.C diff --git a/gcc/cp/cp-tree.h b/gcc/cp/cp-tree.h index 369d2a9119d..05b0d2ac19f 100644 --- a/gcc/cp/cp-tree.h +++ b/gcc/cp/cp-tree.h @@ -2133,6 +2133,7 @@ struct GTY(()) saved_scope { bool expansion_stmt : 1; int unevaluated_operand; + int unevaluated_typeid_cutoff; int inhibit_evaluation_warnings; int noexcept_operand; int ref_temp_count; @@ -6303,13 +6304,25 @@ extern bool cp_preserve_using_decl; extern int cp_unevaluated_operand; +/* cp_unevaluated_operand depth up to which lambda capture is still + allowed. A typeid operand's own unevaluated probe raises this so + that probe does not by itself suppress capture + ([expr.prim.lambda.capture]/7). */ + +extern int cp_unevaluated_typeid_cutoff; + /* RAII class used to inhibit the evaluation of operands during parsing - and template instantiation. Evaluation warnings are also inhibited. */ + and template instantiation. Evaluation warnings are also inhibited. + TYPEID_OPERAND: this is a typeid operand's own probe, so it should + not by itself suppress lambda capture + ([expr.prim.lambda.capture]/7) - see cp_unevaluated_typeid_cutoff. */ class cp_unevaluated { public: - cp_unevaluated (); + int saved_cutoff; + + cp_unevaluated (bool typeid_operand = false); ~cp_unevaluated (); }; @@ -6321,13 +6334,19 @@ class cp_evaluated public: int uneval; int inhibit; + int typeid_cutoff; cp_evaluated (bool reset = true) - : uneval(cp_unevaluated_operand), inhibit(c_inhibit_evaluation_warnings) + : uneval (cp_unevaluated_operand), inhibit (c_inhibit_evaluation_warnings), + typeid_cutoff (cp_unevaluated_typeid_cutoff) { if (reset) - cp_unevaluated_operand = c_inhibit_evaluation_warnings = 0; } + { + cp_unevaluated_operand = c_inhibit_evaluation_warnings = 0; + cp_unevaluated_typeid_cutoff = 0; + } } ~cp_evaluated () { cp_unevaluated_operand = uneval; - c_inhibit_evaluation_warnings = inhibit; } + c_inhibit_evaluation_warnings = inhibit; + cp_unevaluated_typeid_cutoff = typeid_cutoff; } }; /* in pt.cc */ diff --git a/gcc/cp/constraint.cc b/gcc/cp/constraint.cc index fa271a96282..d0d39da7979 100644 --- a/gcc/cp/constraint.cc +++ b/gcc/cp/constraint.cc @@ -1703,9 +1703,12 @@ tsubst_constraint_variables (tree t, tree args, subst_info info) /* Clear cp_unevaluated_operand across tsubst so that we get a proper chain of PARM_DECLs. */ int saved_unevaluated_operand = cp_unevaluated_operand; + int saved_unevaluated_typeid_cutoff = cp_unevaluated_typeid_cutoff; cp_unevaluated_operand = 0; + cp_unevaluated_typeid_cutoff = 0; tree vars = tsubst (t, args, info.complain, info.in_decl); cp_unevaluated_operand = saved_unevaluated_operand; + cp_unevaluated_typeid_cutoff = saved_unevaluated_typeid_cutoff; if (vars == error_mark_node) return error_mark_node; return declare_constraint_vars (t, vars); diff --git a/gcc/cp/lambda.cc b/gcc/cp/lambda.cc index e0d676cae81..ff76b98d6e7 100644 --- a/gcc/cp/lambda.cc +++ b/gcc/cp/lambda.cc @@ -847,8 +847,10 @@ lambda_expr_this_capture (tree lambda, int add_capture_p) this_capture = spec; } - /* In unevaluated context this isn't an odr-use, so don't capture. */ - if (cp_unevaluated_operand) + /* In unevaluated context this isn't an odr-use, so don't capture. + A typeid operand's own probe is capture-transparent + ([expr.prim.lambda.capture]/7). */ + if (cp_unevaluated_operand > cp_unevaluated_typeid_cutoff) add_capture_p = false; /* If we captured 'this' but don't have a capture proxy yet, look up the diff --git a/gcc/cp/name-lookup.cc b/gcc/cp/name-lookup.cc index 60a6e6f4430..13cee628faa 100644 --- a/gcc/cp/name-lookup.cc +++ b/gcc/cp/name-lookup.cc @@ -9069,6 +9069,7 @@ push_to_top_level (void) s->need_pop_function_context = need_pop; s->function_decl = current_function_decl; s->unevaluated_operand = cp_unevaluated_operand; + s->unevaluated_typeid_cutoff = cp_unevaluated_typeid_cutoff; s->inhibit_evaluation_warnings = c_inhibit_evaluation_warnings; s->suppress_location_wrappers = suppress_location_wrappers; s->x_stmt_tree.stmts_are_full_exprs_p = true; @@ -9080,6 +9081,7 @@ push_to_top_level (void) current_namespace = global_namespace; push_class_stack (); cp_unevaluated_operand = 0; + cp_unevaluated_typeid_cutoff = 0; c_inhibit_evaluation_warnings = 0; suppress_location_wrappers = 0; } @@ -9112,6 +9114,7 @@ pop_from_top_level (void) pop_function_context (); current_function_decl = s->function_decl; cp_unevaluated_operand = s->unevaluated_operand; + cp_unevaluated_typeid_cutoff = s->unevaluated_typeid_cutoff; c_inhibit_evaluation_warnings = s->inhibit_evaluation_warnings; suppress_location_wrappers = s->suppress_location_wrappers; @@ -9129,6 +9132,7 @@ namespace { struct local_state_t { int cp_unevaluated_operand; + int cp_unevaluated_typeid_cutoff; int c_inhibit_evaluation_warnings; int cp_noexcept_operand_; bool has_cfun; @@ -9139,6 +9143,8 @@ struct local_state_t local_state_t s; s.cp_unevaluated_operand = ::cp_unevaluated_operand; ::cp_unevaluated_operand = 0; + s.cp_unevaluated_typeid_cutoff = ::cp_unevaluated_typeid_cutoff; + ::cp_unevaluated_typeid_cutoff = 0; s.c_inhibit_evaluation_warnings = ::c_inhibit_evaluation_warnings; ::c_inhibit_evaluation_warnings = 0; s.cp_noexcept_operand_ = ::cp_noexcept_operand; @@ -9153,6 +9159,7 @@ struct local_state_t restore () const { ::cp_unevaluated_operand = this->cp_unevaluated_operand; + ::cp_unevaluated_typeid_cutoff = this->cp_unevaluated_typeid_cutoff; ::c_inhibit_evaluation_warnings = this->c_inhibit_evaluation_warnings; ::cp_noexcept_operand = this->cp_noexcept_operand_; if (this->has_cfun) diff --git a/gcc/cp/parser.cc b/gcc/cp/parser.cc index 967ba3fd542..634dd410ff7 100644 --- a/gcc/cp/parser.cc +++ b/gcc/cp/parser.cc @@ -296,6 +296,12 @@ static FILE *cp_lexer_debug_stream; sizeof can be nested. */ int cp_unevaluated_operand; +/* cp_unevaluated_operand depth up to which lambda capture is still + allowed; raised by a typeid operand's own unevaluated probe so that + probe does not by itself suppress capture + ([expr.prim.lambda.capture]/7). */ +int cp_unevaluated_typeid_cutoff; + /* Nonzero if we are parsing a reflect-expression and shouldn't strip using-declarations. */ bool cp_preserve_using_decl; @@ -3241,16 +3247,21 @@ static tree cp_parser_omp_loop_nest (cp_parser *, bool *); // Unevaluated Operand Guard // // Implementation of an RAII helper for unevaluated operand parsing. -cp_unevaluated::cp_unevaluated () +cp_unevaluated::cp_unevaluated (bool typeid_operand) + : saved_cutoff (cp_unevaluated_typeid_cutoff) { + bool transparent = (cp_unevaluated_operand == cp_unevaluated_typeid_cutoff); ++cp_unevaluated_operand; ++c_inhibit_evaluation_warnings; + if (typeid_operand && transparent) + cp_unevaluated_typeid_cutoff = cp_unevaluated_operand; } cp_unevaluated::~cp_unevaluated () { --c_inhibit_evaluation_warnings; --cp_unevaluated_operand; + cp_unevaluated_typeid_cutoff = saved_cutoff; } // -------------------------------------------------------------------------- // @@ -8579,7 +8590,7 @@ cp_parser_postfix_expression (cp_parser *parser, bool address_p, bool cast_p, (mark_used -> instantiation; lambda capture). */ cp_lexer_save_tokens (parser->lexer); { - cp_unevaluated u; + cp_unevaluated u (/* typeid_operand= */ true); expression = cp_parser_expression (parser, &idk); } /* If we're already within an unevaluated operand, everything diff --git a/gcc/cp/pt.cc b/gcc/cp/pt.cc index dd0efa54112..28845214eed 100644 --- a/gcc/cp/pt.cc +++ b/gcc/cp/pt.cc @@ -12962,6 +12962,7 @@ instantiate_class_template (tree type) maybe_diagnose_erroneous_template (t ? TI_TEMPLATE (t) : templ); int saved_unevaluated_operand = cp_unevaluated_operand; + int saved_unevaluated_typeid_cutoff = cp_unevaluated_typeid_cutoff; int saved_inhibit_evaluation_warnings = c_inhibit_evaluation_warnings; fn_context = decl_function_context (TYPE_MAIN_DECL (type)); @@ -12973,6 +12974,7 @@ instantiate_class_template (tree type) else { cp_unevaluated_operand = 0; + cp_unevaluated_typeid_cutoff = 0; c_inhibit_evaluation_warnings = 0; } @@ -13390,6 +13392,7 @@ instantiate_class_template (tree type) /* Restore these before substituting into the lambda capture initializers. */ cp_unevaluated_operand = saved_unevaluated_operand; + cp_unevaluated_typeid_cutoff = saved_unevaluated_typeid_cutoff; c_inhibit_evaluation_warnings = saved_inhibit_evaluation_warnings; } @@ -23098,7 +23101,7 @@ tsubst_expr (tree t, tree args, tsubst_flags_t complain, tree in_decl) tree operand; tree uneval; { - cp_unevaluated u; + cp_unevaluated u (/* typeid_operand= */ true); uneval = RECUR (operand_0); } /* If we're already within an unevaluated operand, everything diff --git a/gcc/cp/semantics.cc b/gcc/cp/semantics.cc index bb952e2f62c..f1a52b196a4 100644 --- a/gcc/cp/semantics.cc +++ b/gcc/cp/semantics.cc @@ -4682,15 +4682,21 @@ tree process_outer_var_ref (tree decl, tsubst_flags_t complain, bool odr_use/*=false*/) { + bool in_typeid_probe = false; if (cp_unevaluated_operand) { tree type = TREE_TYPE (decl); if (!dependent_type_p (type) && variably_modified_type_p (type, NULL_TREE)) /* VLAs are used even in unevaluated context. */; - else + else if (cp_unevaluated_operand > cp_unevaluated_typeid_cutoff) /* It's not a use (3.2) if we're in an unevaluated context. */ return decl; + else + /* Inside a typeid operand's own probe: capture is still + attempted below ([expr.prim.lambda.capture]/7), but a missing + capture-default must not be diagnosed yet - see below. */ + in_typeid_probe = true; } if (decl == error_mark_node) return decl; @@ -4777,6 +4783,12 @@ process_outer_var_ref (tree decl, tsubst_flags_t complain, if (flag_contracts && processing_contract_condition) set_contract_capture_flag (decl, true); } + /* Capture was attempted above; whether it's really needed depends on + an evaluated-ness we don't know yet. Defer instead of diagnosing: + a later, non-probe pass will complain if the operand turns out + evaluated, and there's nothing to complain about if it doesn't. */ + else if (in_typeid_probe) + return var; /* Only an odr-use of an outer automatic variable causes an error, and a constant variable can decay to a prvalue constant without odr-use. So don't complain yet. */ diff --git a/gcc/testsuite/g++.dg/rtti/typeid-lambda-capture-pr127132.C b/gcc/testsuite/g++.dg/rtti/typeid-lambda-capture-pr127132.C new file mode 100644 index 00000000000..ccb426db8a4 --- /dev/null +++ b/gcc/testsuite/g++.dg/rtti/typeid-lambda-capture-pr127132.C @@ -0,0 +1,169 @@ +// PR c++/127132: typeid inside a generic lambda's capture-default failed +// to capture, because capture incorrectly depended on whether the +// typeid operand was evaluated ([expr.typeid]/4-5), rather than on +// [expr.prim.lambda.capture]/7 (P0588R1 / CWG1468), which says capture +// must ignore the effect of any enclosing typeid. Regression against +// the PR c++/125886 fix (r17-2200-g621661f7d2e9). +// { dg-do compile { target c++14 } } +// c++14 target: several cases below use a generic lambda's auto +// parameter, valid from C++14 on. + +#include <typeinfo> + +void foo(const std::type_info &type); + +// [expr.typeid]/4: *c is a polymorphic glvalue, so typeid's operand is +// evaluated, and the generic lambda's by-copy capture-default must +// capture c for that use. On trunk this capture is currently missing, +// leaving a decl reference with nothing behind it once the lambda's +// call operator is expanded - crashing at RTL expand time instead of +// compiling cleanly. + +class C { + virtual ~C(); +}; + +void +bar (C *c) +{ + [=](auto a) { foo(typeid(*c)); }(1); +} + +// [expr.prim.lambda.capture]/7 (P0588R1 / CWG1468): capture must ignore +// the effect of enclosing typeid expressions. n is not a polymorphic +// glvalue, so the operand is unevaluated ([expr.typeid]/5), but n is +// still captured. +// +// If this starts to fail that's OK, the compiler is allowed to optimize +// away the capture, but we need to control it with -fabi-version. + +void +capture_regardless_of_evaluated (int n) +{ + auto lam = [=] { (void) typeid (n); }; + static_assert (sizeof (lam) == sizeof (int), ""); +} + +// Generic-lambda variant. + +void +capture_regardless_of_evaluated_generic (int n) +{ + auto lam = [=](auto a) { (void) typeid (n); }; + lam (0); + static_assert (sizeof (lam) == sizeof (int), ""); +} + +// No capture-default and a genuinely unevaluated operand: must not +// diagnose "not captured". + +void +no_capture_default_non_poly (int n) +{ + [] { (void) typeid (n); }; +} + +// [expr.prim.lambda.capture]/7: capture must not depend on whether the +// typeid operand is evaluated. F is final, so [expr.typeid]/4 leaves +// typeid (f) unevaluated, but f is still captured. Generic-lambda +// variant of pr125886.C's final_glvalue. + +struct F final { virtual ~F (); }; + +void +final_glvalue_generic (F &f) +{ + auto lam = [=](auto a) { foo (typeid (f)); }; + lam (1); + static_assert (sizeof (lam) == sizeof (F), ""); +} + +// D::i names no local entity, so [expr.prim.lambda.capture]/7 does not +// apply and a DR613 non-static member id-expression stays unevaluated +// (no capture). Generic-lambda variant of pr125886.C's nsm. + +struct D { int i; }; + +void +nsm_generic () +{ + auto lam = [=](auto a) { foo (typeid (D::i)); }; + lam (1); + static_assert (sizeof (lam) == 1, ""); +} + +// sizeof (typeid (b)) is a genuinely unevaluated context wrapping +// typeid, so the cutoff stays unraised and b is not captured. + +struct B { virtual ~B (); }; + +void +nested_unevaluated_lambda_size (B &b) +{ + auto lam = [=] { (void) sizeof (typeid (b)); }; + static_assert (sizeof (lam) == 1, ""); +} + +// Nested typeid looks through every enclosing typeid +// ([expr.prim.lambda.capture]/7, "any enclosing typeid expressions"). + +void +nested_typeid_capture (int x) +{ + auto lam = [=] { (void) typeid (typeid (x)); }; + static_assert (sizeof (lam) == sizeof (int), ""); +} + +// Extremely nested typeid: look-through on each probe +// (operand == cutoff -> cutoff += 1). + +void +nested_typeid_capture_extreme (int x) +{ + auto lam = [=] { + (void) typeid (typeid (typeid (typeid (typeid (typeid (typeid (typeid (typeid (typeid (typeid (x))))))))))); + }; + static_assert (sizeof (lam) == sizeof (int), ""); +} + +// sizeof does not raise the cutoff, so all 10 typeid probes stay above +// it and x is not captured. + +void +nested_typeid_capture_extreme_v2 (int x) +{ + auto lam = [=] { (void) sizeof (typeid (typeid (typeid (typeid (typeid (typeid (typeid (typeid (typeid (typeid (x))))))))))); }; + static_assert (sizeof (lam) == 1, ""); +} + +// this-capture variant of capture_regardless_of_evaluated: +// [expr.prim.lambda.capture]/7 applies to *this like any other local +// entity. [&] avoids the (unrelated) C++20 deprecation warning for +// implicit this-capture via [=]. + +struct E { + int i; + void this_capture_regardless_of_evaluated (); +}; + +void +E::this_capture_regardless_of_evaluated () +{ + auto lam = [&] { (void) typeid (this->i); }; + static_assert (sizeof (lam) == sizeof (E *), ""); +} + +// Generic-lambda variant. + +struct E2 { + int i; + void this_capture_regardless_of_evaluated_generic (); +}; + +void +E2::this_capture_regardless_of_evaluated_generic () +{ + auto lam = [&](auto a) { (void) typeid (this->i); }; + lam (1); + static_assert (sizeof (lam) == sizeof (E2 *), ""); +} -- 2.55.0
