Made changes according to Jason's Merrill comments.
From 9a42e71cd2244b80327cb37fe372c45b018b5958 Mon Sep 17 00:00:00 2001
From: Vladislav Semykin <[email protected]>
Date: Mon, 6 Jul 2026 13:25:26 +0300
Subject: [PATCH v4] 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.
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 <[email protected]>
---
gcc/cp/cp-tree.h | 1 +
gcc/cp/parser.cc | 26 +++++-
gcc/cp/pt.cc | 17 +++-
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, 201 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..5bb434903 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,28 @@ 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 (expression != error_mark_node
+ && processing_template_decl == 0
+ && 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..40ec4d800 100644
--- a/gcc/cp/pt.cc
+++ b/gcc/cp/pt.cc
@@ -23003,8 +23003,21 @@ 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 (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