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
From 39b4dd31331dda05a6d67815d260023cf44899a5 Mon Sep 17 00:00:00 2001
From: Vladislav Semykin <[email protected]>
Date: Mon, 6 Jul 2026 19:02:06 +0300
Subject: [PATCH] c++: fix unevaluated operand context for typeid [PR125886]
To: [email protected]
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.
PR c++/125886
PR c++/116385
PR c++/68604
gcc/cp/ChangeLog:
* 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 <[email protected]>
---
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 a0fa19a9c37..a3be24b4fb5 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 aeff4535337..b57acc6cba5 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 458ba7efae1..3c110d7fa1c 100644
--- a/gcc/cp/pt.cc
+++ b/gcc/cp/pt.cc
@@ -23011,8 +23011,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 7e6fa51936a..48e4dfde458 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 63dae38dea3..f763b208cc9 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 00000000000..96a7ec3cff1
--- /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 00000000000..422a81fa209
--- /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 00000000000..76478bed3ef
--- /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.54.0