[clang] [Clang][ExprConst] Accept prvalue for nothrow new (PR #226753)
via cfe-commits
cfe-commits at lists.llvm.org
Sun Sep 27 10:27:35 PDT 2026
https://github.com/aokblast updated https://github.com/llvm/llvm-project/pull/226753
>From ff5453f705924f0409df46cc523fcaceedcf459b Mon Sep 17 00:00:00 2001
From: ShengYi Hung <aokblast at FreeBSD.org>
Date: Sat, 26 Sep 2026 23:52:05 -0500
Subject: [PATCH 1/5] [Clang][ExprConst] Accept prvalue for nothrow new
A user defined operator new can accept prvalue for nothrow. There is
no guarantee in spec that it should be lvalue. Relax the check with
side effect only.
---
clang/lib/AST/ExprConstant.cpp | 7 +++++--
clang/test/SemaCXX/new-nothrow-by-value.cpp | 18 ++++++++++++++++++
2 files changed, 23 insertions(+), 2 deletions(-)
create mode 100644 clang/test/SemaCXX/new-nothrow-by-value.cpp
diff --git a/clang/lib/AST/ExprConstant.cpp b/clang/lib/AST/ExprConstant.cpp
index 2df754dc9007f..578ce94e93e3e 100644
--- a/clang/lib/AST/ExprConstant.cpp
+++ b/clang/lib/AST/ExprConstant.cpp
@@ -11030,8 +11030,11 @@ bool PointerExprEvaluator::VisitCXXNewExpr(const CXXNewExpr *E) {
// (which should presumably be valid only if N is a multiple of
// alignof(int), and in any case can't be deallocated unless N is
// alignof(X) and X has new-extended alignment).
- LValue Nothrow;
- if (!EvaluateLValue(E->getPlacementArg(0), Nothrow, Info))
+ //
+ // The argument is usually an lvalue (the standard signature takes a
+ // 'const std::nothrow_t &'), but a user-declared allocation function may
+ // take 'std::nothrow_t' by value, in which case it is a prvalue.
+ if (!EvaluateIgnoredValue(Info, E->getPlacementArg(0)))
return false;
IsNothrow = true;
} else if (OperatorNew->isReservedGlobalPlacementOperator()) {
diff --git a/clang/test/SemaCXX/new-nothrow-by-value.cpp b/clang/test/SemaCXX/new-nothrow-by-value.cpp
new file mode 100644
index 0000000000000..acb70803b020c
--- /dev/null
+++ b/clang/test/SemaCXX/new-nothrow-by-value.cpp
@@ -0,0 +1,18 @@
+// RUN: %clang_cc1 -std=c++20 -fsyntax-only -verify %s
+
+// The constant evaluator used to assume the (std::nothrow) placement argument
+// is always an lvalue and crashed when a user-declared allocation function
+// takes std::nothrow_t by value, making the argument a prvalue.
+
+namespace std {
+inline constexpr struct nothrow_t {
+} nothrow;
+} // namespace std
+
+void *operator new[](unsigned long, std::nothrow_t) noexcept;
+void *operator new(unsigned long, std::nothrow_t) noexcept;
+
+void set(int *p) {
+ p = (1 ? new (std::nothrow) int[1] : nullptr);
+ p = (1 ? new (std::nothrow) int : nullptr);
+}
>From ef60af81d777f6a8cbd8a428b7bc7bccad6d1deb Mon Sep 17 00:00:00 2001
From: ShengYi Hung <aokblast at FreeBSD.org>
Date: Sun, 27 Sep 2026 00:29:09 -0500
Subject: [PATCH 2/5] fixup! [Clang][ExprConst] Accept prvalue for nothrow new
---
clang/test/SemaCXX/new-nothrow-by-value.cpp | 1 +
1 file changed, 1 insertion(+)
diff --git a/clang/test/SemaCXX/new-nothrow-by-value.cpp b/clang/test/SemaCXX/new-nothrow-by-value.cpp
index acb70803b020c..569ccec204530 100644
--- a/clang/test/SemaCXX/new-nothrow-by-value.cpp
+++ b/clang/test/SemaCXX/new-nothrow-by-value.cpp
@@ -1,4 +1,5 @@
// RUN: %clang_cc1 -std=c++20 -fsyntax-only -verify %s
+// RUN: %clang_cc1 -std=c++20 -fsyntax-only -verify %s -fexperimental-new-constant-interpreter
// The constant evaluator used to assume the (std::nothrow) placement argument
// is always an lvalue and crashed when a user-declared allocation function
>From c514037b10d0bf7a75f6a3f4e39cadf47440b622 Mon Sep 17 00:00:00 2001
From: ShengYi Hung <aokblast at FreeBSD.org>
Date: Sun, 27 Sep 2026 01:23:48 -0500
Subject: [PATCH 3/5] fixup! [Clang][ExprConst] Accept prvalue for nothrow new
---
clang/test/SemaCXX/new-nothrow-by-value.cpp | 1 +
1 file changed, 1 insertion(+)
diff --git a/clang/test/SemaCXX/new-nothrow-by-value.cpp b/clang/test/SemaCXX/new-nothrow-by-value.cpp
index 569ccec204530..5bd784b157d5e 100644
--- a/clang/test/SemaCXX/new-nothrow-by-value.cpp
+++ b/clang/test/SemaCXX/new-nothrow-by-value.cpp
@@ -1,5 +1,6 @@
// RUN: %clang_cc1 -std=c++20 -fsyntax-only -verify %s
// RUN: %clang_cc1 -std=c++20 -fsyntax-only -verify %s -fexperimental-new-constant-interpreter
+// expected-no-diagnostics
// The constant evaluator used to assume the (std::nothrow) placement argument
// is always an lvalue and crashed when a user-declared allocation function
>From 220b5f54e2e2cca92e36d7e0033f0000e8381073 Mon Sep 17 00:00:00 2001
From: ShengYi Hung <aokblast at FreeBSD.org>
Date: Sun, 27 Sep 2026 02:02:22 -0500
Subject: [PATCH 4/5] fixup! [Clang][ExprConst] Accept prvalue for nothrow new
---
clang/test/SemaCXX/new-nothrow-by-value.cpp | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/clang/test/SemaCXX/new-nothrow-by-value.cpp b/clang/test/SemaCXX/new-nothrow-by-value.cpp
index 5bd784b157d5e..2f7af00b9316b 100644
--- a/clang/test/SemaCXX/new-nothrow-by-value.cpp
+++ b/clang/test/SemaCXX/new-nothrow-by-value.cpp
@@ -11,8 +11,8 @@ inline constexpr struct nothrow_t {
} nothrow;
} // namespace std
-void *operator new[](unsigned long, std::nothrow_t) noexcept;
-void *operator new(unsigned long, std::nothrow_t) noexcept;
+void *operator new[](__SIZE_TYPE__, std::nothrow_t) noexcept;
+void *operator new(__SIZE_TYPE__, std::nothrow_t) noexcept;
void set(int *p) {
p = (1 ? new (std::nothrow) int[1] : nullptr);
>From 5402c8aa4637f3295159df5765b7c505e467adf2 Mon Sep 17 00:00:00 2001
From: ShengYi Hung <aokblast at FreeBSD.org>
Date: Sun, 27 Sep 2026 12:27:17 -0500
Subject: [PATCH 5/5] fixup! [Clang][ExprConst] Accept prvalue for nothrow new
---
clang/lib/AST/ByteCode/Compiler.cpp | 6 ++-
clang/lib/AST/ByteCode/Interp.cpp | 10 +++++
clang/lib/AST/ExprConstant.cpp | 45 +++++++++++----------
clang/test/SemaCXX/new-nothrow-by-value.cpp | 17 ++++++--
4 files changed, 52 insertions(+), 26 deletions(-)
diff --git a/clang/lib/AST/ByteCode/Compiler.cpp b/clang/lib/AST/ByteCode/Compiler.cpp
index 81c8fb0b9f17d..408ff5ca446e4 100644
--- a/clang/lib/AST/ByteCode/Compiler.cpp
+++ b/clang/lib/AST/ByteCode/Compiler.cpp
@@ -4275,7 +4275,11 @@ bool Compiler<Emitter>::VisitCXXNewExpr(const CXXNewExpr *E) {
// alignof(X) and X has new-extended alignment).
if (PlacementArgs == 1) {
const Expr *Arg1 = E->getPlacementArg(0);
- if (Arg1->getType()->isNothrowT()) {
+ // A user-declared allocation function taking std::nothrow_t (for
+ // instance by value) is not usable in a constant expression.
+ if (Arg1->getType()->isNothrowT() &&
+ OperatorNew
+ ->isUsableAsGlobalAllocationFunctionInConstantEvaluation()) {
if (!this->discard(Arg1))
return false;
IsNoThrow = true;
diff --git a/clang/lib/AST/ByteCode/Interp.cpp b/clang/lib/AST/ByteCode/Interp.cpp
index f885c0dc9cdb2..4bc1d012f71b7 100644
--- a/clang/lib/AST/ByteCode/Interp.cpp
+++ b/clang/lib/AST/ByteCode/Interp.cpp
@@ -2935,6 +2935,16 @@ bool InvalidNewDeleteExpr(InterpState &S, CodePtr OpPC, const Expr *E) {
const FunctionDecl *OperatorNew = NewExpr->getOperatorNew();
if (NewExpr->getNumPlacementArgs() > 0) {
+ if (NewExpr->getNumPlacementArgs() == 1 &&
+ NewExpr->getPlacementArg(0)->getType()->isNothrowT() &&
+ !OperatorNew
+ ->isUsableAsGlobalAllocationFunctionInConstantEvaluation()) {
+ S.FFDiag(S.Current->getSource(OpPC),
+ diag::note_constexpr_new_non_replaceable)
+ << isa<CXXMethodDecl>(OperatorNew) << OperatorNew;
+ return false;
+ }
+
// This is allowed pre-C++26, but only an std function or if
// [[msvc::constexpr]] was used.
if (S.getLangOpts().CPlusPlus26 || S.Current->isStdFunction() ||
diff --git a/clang/lib/AST/ExprConstant.cpp b/clang/lib/AST/ExprConstant.cpp
index 636a6a2449ebb..f7faffc767c1c 100644
--- a/clang/lib/AST/ExprConstant.cpp
+++ b/clang/lib/AST/ExprConstant.cpp
@@ -11018,26 +11018,21 @@ bool PointerExprEvaluator::VisitCXXNewExpr(const CXXNewExpr *E) {
bool IsNothrow = false;
bool IsPlacement = false;
- if (E->getNumPlacementArgs() == 1 &&
- E->getPlacementArg(0)->getType()->isNothrowT()) {
- // The only new-placement list we support is of the form (std::nothrow).
- //
- // FIXME: There is no restriction on this, but it's not clear that any
- // other form makes any sense. We get here for cases such as:
- //
- // new (std::align_val_t{N}) X(int)
- //
- // (which should presumably be valid only if N is a multiple of
- // alignof(int), and in any case can't be deallocated unless N is
- // alignof(X) and X has new-extended alignment).
- //
- // The argument is usually an lvalue (the standard signature takes a
- // 'const std::nothrow_t &'), but a user-declared allocation function may
- // take 'std::nothrow_t' by value, in which case it is a prvalue.
- if (!EvaluateIgnoredValue(Info, E->getPlacementArg(0)))
- return false;
- IsNothrow = true;
- } else if (OperatorNew->isReservedGlobalPlacementOperator()) {
+ // The only new-placement list we support (other than the reserved placement
+ // form) is of the form (std::nothrow).
+ //
+ // FIXME: There is no restriction on this, but it's not clear that any
+ // other form makes any sense. We get here for cases such as:
+ //
+ // new (std::align_val_t{N}) X(int)
+ //
+ // (which should presumably be valid only if N is a multiple of
+ // alignof(int), and in any case can't be deallocated unless N is
+ // alignof(X) and X has new-extended alignment).
+ bool HasNothrowArg = E->getNumPlacementArgs() == 1 &&
+ E->getPlacementArg(0)->getType()->isNothrowT();
+
+ if (OperatorNew->isReservedGlobalPlacementOperator()) {
if (Info.CurrentCall->isStdFunction() || Info.getLangOpts().CPlusPlus26 ||
(Info.CurrentCall->CanEvalMSConstexpr &&
OperatorNew->hasAttr<MSConstexprAttr>())) {
@@ -11052,15 +11047,23 @@ bool PointerExprEvaluator::VisitCXXNewExpr(const CXXNewExpr *E) {
<< /*C++26 feature*/ 1 << E->getSourceRange();
return false;
}
- } else if (E->getNumPlacementArgs()) {
+ } else if (E->getNumPlacementArgs() && !HasNothrowArg) {
Info.FFDiag(E, diag::note_constexpr_new_placement)
<< /*Unsupported*/ 0 << E->getSourceRange();
return false;
} else if (!OperatorNew
->isUsableAsGlobalAllocationFunctionInConstantEvaluation()) {
+ // [expr.const] only permits new-expressions that select a replaceable
+ // global allocation function. Check this before evaluating a
+ // (std::nothrow) placement argument.
Info.FFDiag(E, diag::note_constexpr_new_non_replaceable)
<< isa<CXXMethodDecl>(OperatorNew) << OperatorNew;
return false;
+ } else if (HasNothrowArg) {
+ LValue Nothrow;
+ if (!EvaluateLValue(E->getPlacementArg(0), Nothrow, Info))
+ return false;
+ IsNothrow = true;
}
const Expr *Init = E->getInitializer();
diff --git a/clang/test/SemaCXX/new-nothrow-by-value.cpp b/clang/test/SemaCXX/new-nothrow-by-value.cpp
index 2f7af00b9316b..c2985ad582219 100644
--- a/clang/test/SemaCXX/new-nothrow-by-value.cpp
+++ b/clang/test/SemaCXX/new-nothrow-by-value.cpp
@@ -1,10 +1,12 @@
// RUN: %clang_cc1 -std=c++20 -fsyntax-only -verify %s
// RUN: %clang_cc1 -std=c++20 -fsyntax-only -verify %s -fexperimental-new-constant-interpreter
-// expected-no-diagnostics
-// The constant evaluator used to assume the (std::nothrow) placement argument
-// is always an lvalue and crashed when a user-declared allocation function
-// takes std::nothrow_t by value, making the argument a prvalue.
+// [expr.const] only permits a new-expression in a constant expression when it
+// selects a replaceable global allocation function. None of them take
+// std::nothrow_t by value, so a user-declared allocation function that does
+// must be rejected before examining its placement argument. The constant
+// evaluator used to assume the argument was always an lvalue and crashed on
+// the prvalue produced here.
namespace std {
inline constexpr struct nothrow_t {
@@ -18,3 +20,10 @@ void set(int *p) {
p = (1 ? new (std::nothrow) int[1] : nullptr);
p = (1 ? new (std::nothrow) int : nullptr);
}
+
+constexpr bool by_value() { // expected-error {{constexpr function never produces a constant expression}}
+ int *p = new (std::nothrow) int; // expected-note 2{{call to placement 'operator new'}}
+ delete p;
+ return true;
+}
+static_assert(by_value()); // expected-error {{not an integral constant expression}} expected-note {{in call to 'by_value()'}}
More information about the cfe-commits
mailing list