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

Reply via email to