[clang] [Clang] Keep the immediate-invocation wrapper on a reused CXXTemporaryObjectExpr during instantiation (PR #221816)
via cfe-commits
cfe-commits at lists.llvm.org
Mon Sep 7 13:01:08 PDT 2026
llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-clang
Author: Stanislav Bardyuk (kodlan)
<details>
<summary>Changes</summary>
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
---
Full diff: https://github.com/llvm/llvm-project/pull/221816.diff
3 Files Affected:
- (modified) clang/docs/ReleaseNotes.md (+3)
- (modified) clang/lib/Sema/TreeTransform.h (+6-1)
- (modified) clang/test/CodeGenCXX/cxx20-consteval-crash.cpp (+47)
``````````diff
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
``````````
</details>
https://github.com/llvm/llvm-project/pull/221816
More information about the cfe-commits
mailing list