[v5] cp: fix unevaluated operand context for typeid [PR c++/125886]

Message ID CAOVfs5-zvwcxSH5Lg2+e9VEsnofRxzubP1kXWu5eDRnwnjNtZg@mail.gmail.com
State New
Headers
Series [v5] cp: fix unevaluated operand context for typeid [PR c++/125886] |

Checks

Context Check Description
linaro-tcwg-bot/tcwg_gcc_build--master-arm success Build passed
linaro-tcwg-bot/tcwg_gcc_build--master-aarch64 pending Build did not complete; will be retriggered
linaro-tcwg-bot/tcwg_simplebootstrap_build--master-arm-bootstrap pending Build did not complete; will be retriggered

Commit Message

Vladislav Semykin July 6, 2026, 4:08 p.m. UTC
  [image: image.png]

Also, I want to ask the team about links on standard in my comments.
I don't quite understand why the gcc-style doesn't allow a space before
the square bracket; I'll leave it as is for now. But what does the team
prefer?
How appropriate are my references to the standard?
  

Comments

Jason Merrill July 7, 2026, 4:40 p.m. UTC | #1
On 7/6/26 12:08 PM, Vladislav Semykin wrote:
> image.png
> 
> Also, I want to ask the team about links on standard in my comments.
> I don't quite understand why the gcc-style doesn't allow a space before
> the square bracket; I'll leave it as is for now. But what does the team 
> prefer?
> How appropriate are my references to the standard?
Those are false positives from gcc-style, it isn't context-sensitive 
enough; that rule only has to do with [ used for array indexing.  The 
comment is fine as it is.

Your subject line was better in v3: the usual convention is "c++:" and 
the PR number in the subject line doesn't include the Bugzilla 
component.  Why did you change those?

In future, the "Changes since" and testing information should go in the 
email rather than the commit message.

There were still a bunch of indentation issues that I needed to adjust.

After adjusting those minor issues, so here's what I'm pushing.

Thanks!

Jason
  

Patch

From 69f31589451dad35eb144eacdc9e2cdd1ce8b041 Mon Sep 17 00:00:00 2001
From: Vladislav Semykin <vladislav.semykin@gmail.com>
Date: Mon, 6 Jul 2026 19:02:06 +0300
Subject: [PATCH v5] cp: fix unevaluated operand context for typeid [PR
 c++/125886]

Per [expr.typeid]/4-5, a typeid operand is unevaluated by default and
is evaluated only for a glvalue of polymorphic class type whose dynamic
type is not known at compile time.  Previously GCC always parsed the
operand in an evaluated context, which broke unevaluated uses (declval,
non-static data members per DR613, function parameters) and missed
lambda capture diagnostics for evaluated polymorphic operands.

Implement a two-pass parse: first under cp_unevaluated, then - if
typeid_evaluated_p says the operand is evaluated - roll back and
re-parse under cp_evaluated.  Share the evaluated/unevaluated predicate
via typeid_evaluated_p in rtti.cc, used from the parser, tsubst_expr,
and build_typeid.

Also fixes PR c++/68604 and PR c++/116385, and removes a now-stale xfail
in g++.dg/coroutines/unevaluated.C.

Changes since v1:
- Apply the same two-pass logic to template-dependent operands in
  tsubst_expr (TYPEID_EXPR), as Jason requested.
- Fix lambda capture: a polymorphic glvalue typeid operand inside a
  lambda without a capture-default is now correctly diagnosed as
  "not captured".  Root cause was typeid_evaluated_p rejecting
  reference types and being gated on the "nonnull" flag, which is
  unrelated to the evaluated/unevaluated decision.
- Factor the polymorphic-glvalue predicate into shared typeid_evaluated_p
  used by the parser, tsubst, and build_typeid.
- Drop the cxx_dialect >= cxx11 gate: per Jason, DR613 applies in all
  modes, since finish_non_static_data_member doesn't check cxx_dialect
  either.

Changes since v2:
- Restored the full commit message (rationale + ChangeLog entries).
- Shortened cp_unevaluated_operand comments in cp-tree.h and parser.cc
  (git gcc-style).
- Fixed indentation in parser.cc and rtti.cc (git gcc-style).
- Use cp_unevaluated RAII in cp_parser_postfix_expression instead of
  manual ++/--cp_unevaluated_operand, matching tsubst_expr.
- Kept glvalue_p in typeid_evaluated_p: [expr.typeid]/4 applies only to
  a glvalue of polymorphic class type; a polymorphic class prvalue can
  have resolves_to_fixed_type_p false yet still be unevaluated (see
  "Not a glvalue" in g++.dg/cpp2a/constexpr-typeid2.C).  Without
  glvalue_p that case would incorrectly take the evaluated re-parse
  path.
- Investigated the g++.dg/modules/compile-std1.C failure Jason flagged
  as surprising (reproduced with
  'make check-c++ RUNTESTFLAGS="modules.exp=compile-std1.C"').
  g++.log shows cc1plus: fatal error: bits/std.cc: No such file or directory
  and the same for bits/std.compat.cc under --compile-std-module; import
  std then fails because gcm.cache/std.gcm was never built.  Here the
  libstdc++ symlinks at .../include/bits/std.{,compat.}cc point at
  src/c++23/std.{,compat.}cc, but those generated files are missing
  (only .in templates remain) - incomplete libstdc++ modules build, not
  a cp/typeid regression.  Unrelated to this patch.

Changes since v3:
- Removed cp_evaluated on the re-parse path in both
  cp_parser_postfix_expression and tsubst_expr.  As Jason pointed
  out, the re-parse should not override the typeid itself being
  within an outer unevaluated operand (e.g. sizeof (typeid (b)));
  it should just drop back to whatever context enclosed it.
- Added a comment on the glvalue_p check in typeid_evaluated_p
  noting it's only needed because resolves_to_fixed_type_p doesn't
  handle all prvalue cases (e.g. COMPOUND_EXPR), per Jason's
  observation.
- Removed a stale test (lambda_sizeof_typeid) that encoded the old,
  incorrect behavior (requiring capture for typeid nested inside
  sizeof); added nested_unevaluated and nested_unevaluated_lambda
  to cover the corrected behavior instead.

Changes since v4:
- Added a !cp_unevaluated_operand check to the condition in both
  cp_parser_postfix_expression and tsubst_expr, as Marek pointed
  out.  If typeid is already within an outer unevaluated operand
  (e.g. sizeof (typeid (b))), the evaluated re-parse can never
  change anything: per [basic.def.odr]/3, an unevaluated operand's
  subexpressions stay not potentially evaluated regardless of
  [expr.typeid]/4, so the second parse would just redo the same
  tokens under the same ambient context and risk duplicated
  diagnostics.  Covered by the existing nested_unevaluated and
  nested_unevaluated_lambda tests in pr125886.C.

Tested full CI on: x86_64 GNU/Linux 6.17.0-35-generic Ubuntu 24.04.
The following failures are pre-existing on trunk without this patch
and are unrelated to the changes here (full log via
'make -C gcc -k check-c++-all' attached):

FAIL: c-c++-common/analyzer/flex-without-call-summaries.c -std=c++11
  at line 885 (test for warnings, line 884)
XPASS: c-c++-common/analyzer/flex-without-call-summaries.c -std=c++11
  PR analyzer/103546 (test for bogus messages, line 892)
FAIL: c-c++-common/analyzer/flex-without-call-summaries.c -std=c++11
  (test for excess errors)
FAIL: g++.dg/guality/pr55665.C -O2 -flto -fno-use-linker-plugin
  -flto-partition=none line 23 p == 40
FAIL: g++.dg/modules/compile-std1.C -std=c++26 -fimplicit-constexpr
  (test for excess errors)
FAIL: g++.dg/modules/xtreme-header-8.C -std=c++26 -fimplicit-constexpr
  (test for excess errors)
FAIL: g++.dg/modules/xtreme-header-2_a.H -std=c++26 -fimplicit-constexpr
  (test for excess errors)
FAIL: g++.dg/modules/xtreme-header-2_c.C -std=c++26 -fimplicit-constexpr
  (test for excess errors)
FAIL: g++.dg/modules/xtreme-header-7_a.H -std=c++26 -fimplicit-constexpr
  (test for excess errors)
FAIL: g++.dg/modules/xtreme-header_a.H -std=c++26 -fimplicit-constexpr
  (test for excess errors)
FAIL: g++.dg/plugin/std-module-exports-c++20.C
  -fplugin=./std_module_exports_plugin.so (test for excess errors)
FAIL: g++.dg/plugin/std-module-exports-c++23.C
  -fplugin=./std_module_exports_plugin.so (test for excess errors)
FAIL: g++.dg/plugin/std-module-exports-c++26.C
  -fplugin=./std_module_exports_plugin.so (test for excess errors)

gcc/cp/ChangeLog:
	PR c++/125886
	* parser.cc (cp_parser_postfix_expression): Two-pass typeid parse.
	* pt.cc (tsubst_expr): Same for TYPEID_EXPR.
	* rtti.cc (typeid_evaluated_p, build_typeid): Shared predicate.
	* cp-tree.h: Declare typeid_evaluated_p.

gcc/testsuite/ChangeLog:
	* g++.dg/cpp0x/pr125886.C: New test.
	* g++.dg/rtti/typeid14.C, g++.dg/rtti/typeid15.C: New tests.
	* g++.dg/coroutines/unevaluated.C: Drop stale xfail.

Signed-off-by: Vladislav Semykin <vladislav.semykin@gmail.com>
---
 gcc/cp/cp-tree.h                              |  1 +
 gcc/cp/parser.cc                              | 31 ++++++-
 gcc/cp/pt.cc                                  | 21 ++++-
 gcc/cp/rtti.cc                                | 48 ++++++++---
 gcc/testsuite/g++.dg/coroutines/unevaluated.C |  3 +-
 gcc/testsuite/g++.dg/cpp0x/pr125886.C         | 82 +++++++++++++++++++
 gcc/testsuite/g++.dg/rtti/typeid14.C          | 19 +++++
 gcc/testsuite/g++.dg/rtti/typeid15.C          | 23 ++++++
 8 files changed, 210 insertions(+), 18 deletions(-)
 create mode 100644 gcc/testsuite/g++.dg/cpp0x/pr125886.C
 create mode 100644 gcc/testsuite/g++.dg/rtti/typeid14.C
 create mode 100644 gcc/testsuite/g++.dg/rtti/typeid15.C

diff --git a/gcc/cp/cp-tree.h b/gcc/cp/cp-tree.h
index 132139f9d..df0f253cc 100644
--- a/gcc/cp/cp-tree.h
+++ b/gcc/cp/cp-tree.h
@@ -7486,6 +7486,7 @@  extern tree current_nonlambda_class_type	(void);
 extern tree finish_struct			(tree, tree);
 extern void finish_struct_1			(tree);
 extern int resolves_to_fixed_type_p		(tree, int * = NULL);
+extern bool typeid_evaluated_p (tree);
 extern void init_class_processing		(void);
 extern int is_empty_class			(tree);
 extern bool is_really_empty_class		(tree, bool);
diff --git a/gcc/cp/parser.cc b/gcc/cp/parser.cc
index 8194106c6..8ca806748 100644
--- a/gcc/cp/parser.cc
+++ b/gcc/cp/parser.cc
@@ -292,7 +292,8 @@  static void missing_template_diag
 static FILE *cp_lexer_debug_stream;
 
 /* Nonzero if we are parsing an unevaluated operand: an operand to
-   sizeof, typeof, or alignof.  */
+   sizeof, typeof, or alignof.  This is a count since operands to
+   sizeof can be nested.  */
 int cp_unevaluated_operand;
 
 /* Nonzero if we are parsing a reflect-expression and shouldn't strip
@@ -8570,9 +8571,33 @@  cp_parser_postfix_expression (cp_parser *parser, bool address_p, bool cast_p,
 	else
 	  {
 	    tree expression;
+	    /* [expr.typeid]/4-5: parse the operand unevaluated first; if it is
+	       a polymorphic glvalue, roll back and re-parse it evaluated,
+	       since an evaluated parse has irreversible side-effects
+	       (mark_used -> instantiation; lambda capture).  */
+	    cp_lexer_save_tokens (parser->lexer);
+	    {
+	      cp_unevaluated u;
+	      expression = cp_parser_expression (parser, &idk);
+	    }
+	      /* If we're already within an unevaluated operand, everything
+		       in the subtree stays not potentially evaluated regardless
+		       of [expr.typeid]/4 ([basic.def.odr]/3), so the evaluated
+		       re-parse below can have nothing to do; skip it.  */
+	    if (expression != error_mark_node
+		&& processing_template_decl == 0
+	  && !cp_unevaluated_operand
+		&& typeid_evaluated_p (expression))
+	      {
+		/* Re-parse the operand evaluated so the /4 side-effects occur.
+		   The unevaluated pass above called no mark_used and captured
+		   nothing, so rolling back has nothing to undo.  */
+		cp_lexer_rollback_tokens (parser->lexer);
+		expression = cp_parser_expression (parser, &idk);
+	      }
+	    else
+	      cp_lexer_commit_tokens (parser->lexer);
 
-	    /* Look for an expression.  */
-	    expression = cp_parser_expression (parser, & idk);
 	    /* Compute its typeid.  */
 	    postfix_expression = build_typeid (expression, tf_warning_or_error);
 	    /* Look for the `)' token.  */
diff --git a/gcc/cp/pt.cc b/gcc/cp/pt.cc
index f7aa10226..53059b6fc 100644
--- a/gcc/cp/pt.cc
+++ b/gcc/cp/pt.cc
@@ -23003,8 +23003,25 @@  tsubst_expr (tree t, tree args, tsubst_flags_t complain, tree in_decl)
 	  }
 	else
 	  {
-	    operand_0 = RECUR (operand_0);
-	    RETURN (build_typeid (operand_0, complain));
+	    /* [expr.typeid]/4-5: substitute the operand unevaluated first, then
+	       again evaluated if it is a polymorphic glvalue, so the /4
+	       side-effects occur.  The unevaluated pass instantiates nothing,
+	       so re-substituting has nothing to undo (PR c++/125886).  */
+	    tree operand;
+	    tree uneval;
+	      {
+		cp_unevaluated u;
+		uneval = RECUR (operand_0);
+	      }
+	    /* If we're already within an unevaluated operand, everything
+	       in the subtree stays not potentially evaluated regardless
+	       of [expr.typeid]/4 ([basic.def.odr]/3), so the evaluated
+	       re-parse below can have nothing to do; skip it.  */
+	    if (!cp_unevaluated_operand && typeid_evaluated_p (uneval))
+	      operand = RECUR (operand_0);
+	    else
+	      operand = uneval;
+	    RETURN (build_typeid (operand, complain));
 	  }
       }
 
diff --git a/gcc/cp/rtti.cc b/gcc/cp/rtti.cc
index 7e6fa5193..bb38758c0 100644
--- a/gcc/cp/rtti.cc
+++ b/gcc/cp/rtti.cc
@@ -340,6 +340,32 @@  typeid_ok_p (void)
   return true;
 }
 
+/* True if EXP is a glvalue expression of polymorphic class type whose
+   dynamic type is not known statically, so that typeid (EXP) must be
+   evaluated per ([expr.typeid]/4).  */
+
+bool
+typeid_evaluated_p (tree exp)
+{
+  if (exp == error_mark_node)
+    return false;
+  tree t = TREE_TYPE (exp);
+  if (!t || t == error_mark_node)
+    return false;
+  if (TYPE_REF_P (t))
+    t = TREE_TYPE (t);
+  if (TREE_CODE (t) != RECORD_TYPE && TREE_CODE (t) != UNION_TYPE)
+    return false;
+  int nonnull = 0;
+  return (TYPE_POLYMORPHIC_P (t)
+	  && !resolves_to_fixed_type_p (exp, &nonnull)
+    /* Only a glvalue operand is evaluated ([expr.typeid]/4).
+       The following check is only necessary because
+       resolves_to_fixed_type_p does not handle all
+       prvalue cases such as COMPOUND_EXPR.  */
+	  && glvalue_p (exp));
+}
+
 /* Return an expression for "typeid(EXP)".  The expression returned is
    an lvalue of type "const std::type_info".  */
 
@@ -347,7 +373,6 @@  tree
 build_typeid (tree exp, tsubst_flags_t complain)
 {
   tree cond = NULL_TREE, initial_expr = exp;
-  int nonnull = 0;
 
   if (exp == error_mark_node || !typeid_ok_p ())
     return error_mark_node;
@@ -355,17 +380,18 @@  build_typeid (tree exp, tsubst_flags_t complain)
   if (processing_template_decl)
     return build_min (TYPEID_EXPR, const_type_info_type_node, exp);
 
-  if (CLASS_TYPE_P (TREE_TYPE (exp))
-      && TYPE_POLYMORPHIC_P (TREE_TYPE (exp))
-      && ! resolves_to_fixed_type_p (exp, &nonnull)
-      && ! nonnull)
+  if (typeid_evaluated_p (exp))
     {
-      /* So we need to look into the vtable of the type of exp.
-         Make sure it isn't a null lvalue.  */
-      exp = cp_build_addr_expr (exp, complain);
-      exp = save_expr (exp);
-      cond = cp_convert (boolean_type_node, exp, complain);
-      exp = cp_build_fold_indirect_ref (exp);
+      int nonnull = 0;
+      resolves_to_fixed_type_p (exp, &nonnull);
+      if (!nonnull)
+	{
+	  /* Make sure it isn't a null lvalue; evaluate it once.  */
+	  exp = cp_build_addr_expr (exp, complain);
+	  exp = save_expr (exp);
+	  cond = cp_convert (boolean_type_node, exp, complain);
+	  exp = cp_build_fold_indirect_ref (exp);
+	}
     }
 
   exp = get_tinfo_ptr_dynamic (exp, complain);
diff --git a/gcc/testsuite/g++.dg/coroutines/unevaluated.C b/gcc/testsuite/g++.dg/coroutines/unevaluated.C
index 63dae38de..f763b208c 100644
--- a/gcc/testsuite/g++.dg/coroutines/unevaluated.C
+++ b/gcc/testsuite/g++.dg/coroutines/unevaluated.C
@@ -17,8 +17,7 @@  struct Task {
 // We do not permit co_await, co_yield outside a function, and so uses in
 // noexcept or requirements are covered by that.
 Task foo()  {
-    /* This one will currently fail - see PR68604.  */
-    const std::type_info& ti1 = typeid (co_await std::suspend_never{}); // { dg-error {'co_await' cannot be used in an unevaluated context} "" { xfail *-*-* } }
+    const std::type_info& ti1 = typeid (co_await std::suspend_never{}); // { dg-error {'co_await' cannot be used in an unevaluated context} }
     std::size_t x = sizeof (co_yield (19)); // { dg-error {'co_yield' cannot be used in an unevaluated context} }
     decltype (co_await std::suspend_never{}) A; // { dg-error {'co_await' cannot be used in an unevaluated context} }
     co_return;
diff --git a/gcc/testsuite/g++.dg/cpp0x/pr125886.C b/gcc/testsuite/g++.dg/cpp0x/pr125886.C
new file mode 100644
index 000000000..96a7ec3cf
--- /dev/null
+++ b/gcc/testsuite/g++.dg/cpp0x/pr125886.C
@@ -0,0 +1,82 @@ 
+// PR c++/125886
+// PR c++/68604
+// PR c++/116385
+// { dg-do compile { target c++11 } }
+
+#include <typeinfo>
+#include <utility>
+
+// PR c++/125886: a non-polymorphic operand is an unevaluated operand
+// ([expr.typeid]/5), so std::declval<A>() is not instantiated and its
+// static_assert does not fire.
+struct A {};
+void
+non_poly_declval ()
+{
+  (void) typeid (std::declval<A> ());
+}
+
+// PR c++/125886 (template): the evaluated/unevaluated decision is taken
+// at instantiation time in tsubst; a non-polymorphic operand is
+// unevaluated, so tmpl<A> is well-formed.
+template <typename T>
+void
+tmpl ()
+{
+  (void) typeid (std::declval<T> ());
+}
+
+template void tmpl<A> ();
+
+// PR c++/68604: an id-expression denoting a non-static data member is
+// valid in an unevaluated operand (DR613 / N2253, C++11+).
+struct C { int i; };
+void
+nsm ()
+{
+  (void) typeid (C::i);
+}
+
+// PR c++/116385: function parameters are not odr-used in an unevaluated
+// typeid operand, so they need not be captured and may appear in local
+// classes and default arguments.
+void
+params (int n)
+{
+  [&] { (void) typeid (n); };
+  struct Local { void g () { (void) typeid (n); } };
+  void g (const std::type_info& = typeid (n));
+}
+
+// A final-class polymorphic glvalue resolves to a fixed (static) type,
+// so typeid is unevaluated: no vtable lookup, no odr-use.
+struct F final { virtual ~F (); };
+void
+final_glvalue (F& f)
+{
+  (void) typeid (f);
+}
+
+// Polymorphic glvalue in lambda without capture-default must be captured,
+// since typeid is evaluated.
+struct B { virtual ~B (); };
+void
+lambda_poly_capture (B& b)
+{
+  [] { (void) typeid (b); };  // { dg-error "not captured" }
+}
+
+// typeid itself inside an unevaluated operand: the polymorphic glvalue
+// re-parse must not force evaluation here.
+void
+nested_unevaluated(B &b)
+{
+  (void) sizeof (typeid (b)); // no odr-use of b expected
+}
+
+// lambda variant of nested_unevaluated.
+void
+nested_unevaluated_lambda(B &b)
+{
+  [] { (void) sizeof (typeid (b)); }; // OK: no capture required
+}
diff --git a/gcc/testsuite/g++.dg/rtti/typeid14.C b/gcc/testsuite/g++.dg/rtti/typeid14.C
new file mode 100644
index 000000000..422a81fa2
--- /dev/null
+++ b/gcc/testsuite/g++.dg/rtti/typeid14.C
@@ -0,0 +1,19 @@ 
+// PR c++/125886
+// { dg-do compile { target c++11 } }
+
+#include <typeinfo>
+#include <utility>
+
+// [expr.typeid]/4: a glvalue expression of a polymorphic class type is
+// evaluated, so std::declval<B>() is instantiated and
+// __declval_protector's static_assert fires (the non-template case:
+// the operand is parsed evaluated after the unevaluated probe).
+
+struct B { virtual ~B(); };
+
+void
+non_template ()
+{
+  (void) typeid (std::declval<B> ());
+}
+// { dg-error "static assertion failed: declval" "" { target *-*-* } 0 }
diff --git a/gcc/testsuite/g++.dg/rtti/typeid15.C b/gcc/testsuite/g++.dg/rtti/typeid15.C
new file mode 100644
index 000000000..76478bed3
--- /dev/null
+++ b/gcc/testsuite/g++.dg/rtti/typeid15.C
@@ -0,0 +1,23 @@ 
+// PR c++/125886
+// { dg-do compile { target c++11 } }
+
+#include <typeinfo>
+#include <utility>
+
+// [expr.typeid]/4 for a template-dependent operand: the evaluated/
+// unevaluated decision is taken at instantiation time in tsubst.  For
+// a polymorphic operand the operand is re-substituted evaluated, so
+// std::declval<C>() is instantiated and __declval_protector's
+// static_assert fires.
+
+struct C { virtual ~C(); };
+
+template <typename T>
+void
+tmpl ()
+{
+  (void) typeid (std::declval<T> ());
+}
+
+template void tmpl<C> ();
+// { dg-error "static assertion failed: declval" "" { target *-*-* } 0 }
-- 
2.43.0