[clang] [Clang] Keep the immediate-invocation wrapper on a reused CXXTemporaryObjectExpr during instantiation (PR #221816)

Stanislav Bardyuk via cfe-commits cfe-commits at lists.llvm.org
Mon Sep 7 13:00:13 PDT 2026


https://github.com/kodlan created https://github.com/llvm/llvm-project/pull/221816

When a function template contains a temporary built with a consteval constructor, like `(void)S{1}`, Sema wraps the `CXXTemporaryObjectExpr` in a `ConstantExpr` marked as an immediate invocation and caches the value. `TreeTransform::TransformConstantExpr` drops that wrapper on purpose and expects the subexpression to be rebuilt through Sema, which re-creates it. But when the type, constructor and arguments all come out of the instantiation unchanged, `TransformCXXTemporaryObjectExpr` takes its reuse shortcut and returns the bare node, so the instantiated function ends up with an unwrapped call to the consteval constructor. CodeGen then emits it (and the constructor body), which trips the "trying to emit a call to an immediate function" assertion, or without assertions produces a real call to a consteval function and a link error.

```cpp
consteval void f() {}
struct S { consteval S(int) { f(); } };
template <typename T> void g(T) { (void)S{1}; }
template void g<int>(int);
```

This runs the reused node through `CheckForImmediateInvocation` before `MaybeBindToTemporary`, the same two steps `InitializationSequence::Perform` does when it creates the node, so it gets the same wrapper a rebuilt one would get (and the destructor cleanup of the temporary stays outside the `ConstantExpr`, so it is still emitted). Non-consteval constructors are unaffected since `CheckForImmediateInvocation` returns its input untouched for them.

Besides the braced one-argument form from the issue, the same shortcut is reached for `S(1, 2)`, `S{}`, and the same expressions inside generic lambdas and class template members, so the test covers those too. `S(1)` with a single parenthesized argument is a `CXXFunctionalCastExpr` and was always rebuilt, which is why the reporter saw it working.

The reuse shortcut in `TransformCallExpr` has the same problem for a consteval member function called on a non-dependent object (`g.m()` with `constexpr S g;` inside a template still reaches CodeGen bare, also in clang 18). That is a separate shape and is left for a follow-up.

Fixes #219272

>From cc02deb7fd5be4c42121d4e8fe0bbdebbe5127cc Mon Sep 17 00:00:00 2001
From: Stanislav Bardyuk <sbardyuk at google.com>
Date: Mon, 7 Sep 2026 19:58:29 +0000
Subject: [PATCH] [Clang] Keep the immediate-invocation wrapper on a reused
 CXXTemporaryObjectExpr during instantiation

When a function template contains a temporary built with a consteval
constructor, like (void)S{1}, Sema wraps the CXXTemporaryObjectExpr in a
ConstantExpr marked as an immediate invocation and caches the value.
TreeTransform::TransformConstantExpr drops that wrapper on purpose and
expects the subexpression to be rebuilt through Sema, which re-creates
it. But when the type, constructor and arguments all come out of the
instantiation unchanged, TransformCXXTemporaryObjectExpr takes its reuse
shortcut and returns the bare node, so the instantiated function ends up
with an unwrapped call to the consteval constructor. CodeGen then emits
it (and the constructor body), which trips the "trying to emit a call to
an immediate function" assertion, or without assertions produces a real
call to a consteval function and a link error.

Run the reused node through CheckForImmediateInvocation before
MaybeBindToTemporary, the same two steps InitializationSequence::Perform
does when it creates the node, so it gets the same wrapper a rebuilt one
would get and the destructor cleanup of the temporary stays outside the
ConstantExpr.

Besides the braced one-argument form from the issue, the same shortcut
is reached for S(1, 2), S{}, and the same expressions inside generic
lambdas and class template members.

Fixes #219272
---
 clang/docs/ReleaseNotes.md                    |  3 ++
 clang/lib/Sema/TreeTransform.h                |  7 ++-
 .../test/CodeGenCXX/cxx20-consteval-crash.cpp | 47 +++++++++++++++++++
 3 files changed, 56 insertions(+), 1 deletion(-)

diff --git a/clang/docs/ReleaseNotes.md b/clang/docs/ReleaseNotes.md
index a49971adef86f..d223e5a924c5e 100644
--- a/clang/docs/ReleaseNotes.md
+++ b/clang/docs/ReleaseNotes.md
@@ -645,6 +645,9 @@ features cannot lower the translation-unit ABI level;
   (#GH214128)
 - Fixed a crash when a coroutine keyword appeared inside a mem-initializer on a
   function that is not a constructor. (#GH194298)
+- Fixed a crash (and, without assertions, a call to a consteval function being
+  emitted) when a temporary created with a `consteval` constructor, such as
+  `S{1}`, was instantiated from a function template. (#GH219272)
 
 #### Bug Fixes to AST Handling
 
diff --git a/clang/lib/Sema/TreeTransform.h b/clang/lib/Sema/TreeTransform.h
index c8458fda58a88..75eb954163ab5 100644
--- a/clang/lib/Sema/TreeTransform.h
+++ b/clang/lib/Sema/TreeTransform.h
@@ -16183,7 +16183,12 @@ TreeTransform<Derived>::TransformCXXTemporaryObjectExpr(
       !ArgumentChanged) {
     // FIXME: Instantiation-specific
     SemaRef.MarkFunctionReferenced(E->getBeginLoc(), Constructor);
-    return SemaRef.MaybeBindToTemporary(E);
+    // The immediate-invocation wrapper was stripped by TransformConstantExpr;
+    // put it back before binding the temporary, as SemaInit does.
+    ExprResult Res = SemaRef.CheckForImmediateInvocation(E, Constructor);
+    if (Res.isInvalid())
+      return ExprError();
+    return SemaRef.MaybeBindToTemporary(Res.get());
   }
 
   SourceLocation LParenLoc = T->getTypeLoc().getEndLoc();
diff --git a/clang/test/CodeGenCXX/cxx20-consteval-crash.cpp b/clang/test/CodeGenCXX/cxx20-consteval-crash.cpp
index 9c9324f428bec..f5c01729559ca 100644
--- a/clang/test/CodeGenCXX/cxx20-consteval-crash.cpp
+++ b/clang/test/CodeGenCXX/cxx20-consteval-crash.cpp
@@ -140,3 +140,50 @@ void b() {
 // CHECK-NOT: define {{.*}}foo{{.*}}()
 
 } // namespace GH61142
+
+namespace GH219272 {
+
+consteval void f() {}
+void g();
+
+struct S {
+  consteval S() { f(); }
+  consteval S(int) { f(); }
+  consteval S(int, int) { f(); }
+};
+
+struct D {
+  consteval D(int) { f(); }
+  constexpr ~D() {
+    if (!__builtin_is_constant_evaluated())
+      g();
+  }
+};
+
+template <typename T> void dtor(T) { (void)D{1}; }
+template <typename T> void braces(T) { (void)S{1}; }
+template <typename T> void parens(T) { (void)S(1, 2); }
+template <typename T> void empty_braces(T) { (void)S{}; }
+template <typename T> void lambda(T) { [](auto) { (void)S{1}; }(0); }
+template <typename T> struct C {
+  void m() { (void)S{1}; }
+};
+template <typename T> void member(T) { C<T>{}.m(); }
+
+template void dtor<int>(int);
+template void braces<int>(int);
+template void parens<int>(int);
+template void empty_braces<int>(int);
+template void lambda<int>(int);
+template void member<int>(int);
+
+// The temporary is constant-evaluated, but its destructor still runs.
+// CHECK: define {{.*}} @_ZN8GH2192724dtorIiEEvT_(
+// CHECK-NOT: call {{.*}}GH2192721DC
+// CHECK: call void @_ZN8GH2192721DD1Ev(
+
+// Make sure the consteval constructors are neither called nor emitted.
+// CHECK-NOT: call {{.*}}GH2192721{{S|D}}C
+// CHECK-NOT: define {{.*}}GH2192721{{S|D}}C
+
+} // namespace GH219272



More information about the cfe-commits mailing list