[clang] c52392a - [Clang][ExprConst] Drop PRValue for nothrow new (#226753)
via cfe-commits
cfe-commits at lists.llvm.org
Sun Sep 27 19:14:39 PDT 2026
Author: aokblast
Date: 2026-09-28T10:14:30+08:00
New Revision: c52392a6f464e5aa6bff198e4056ca58e43e0656
URL: https://github.com/llvm/llvm-project/commit/c52392a6f464e5aa6bff198e4056ca58e43e0656
DIFF: https://github.com/llvm/llvm-project/commit/c52392a6f464e5aa6bff198e4056ca58e43e0656.diff
LOG: [Clang][ExprConst] Drop PRValue for nothrow new (#226753)
A user defined operator new can accept prvalue for nothrow. However, it
should not be a ConstExpr. Early returns by
isUsableAsGlobalAllocationFunctionInConstantEvaluation instead of doing
LValue evaluation.
Also, move CheckPlacement new logic into new OpCode. This decouples
checking from Interp.cpp to Compiler.cpp.
Added:
clang/test/SemaCXX/new-nothrow-by-value.cpp
Modified:
clang/lib/AST/ByteCode/Compiler.cpp
clang/lib/AST/ByteCode/Interp.cpp
clang/lib/AST/ByteCode/Interp.h
clang/lib/AST/ByteCode/Opcodes.td
clang/lib/AST/ExprConstant.cpp
Removed:
################################################################################
diff --git a/clang/lib/AST/ByteCode/Compiler.cpp b/clang/lib/AST/ByteCode/Compiler.cpp
index 81c8fb0b9f17d..353fd45d8049c 100644
--- a/clang/lib/AST/ByteCode/Compiler.cpp
+++ b/clang/lib/AST/ByteCode/Compiler.cpp
@@ -4275,19 +4275,21 @@ 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()) {
+ if (OperatorNew->isReservedGlobalPlacementOperator()) {
+ if (!this->emitCheckPlacementNew(E, E))
+ return false;
+ PlacementDest = Arg1;
+ } else if (
+ Arg1->getType()->isNothrowT() &&
+ OperatorNew
+ ->isUsableAsGlobalAllocationFunctionInConstantEvaluation()) {
if (!this->discard(Arg1))
return false;
IsNoThrow = true;
} else {
- // Invalid unless we have C++26 or are in a std:: function.
- if (!this->emitInvalidNewDeleteExpr(E, E))
- return false;
-
- // If we have a placement-new destination, we'll later use that instead
- // of allocating.
- if (OperatorNew->isReservedGlobalPlacementOperator())
- PlacementDest = Arg1;
+ // Any other placement list is invalid. This includes a user-declared
+ // allocation function taking std::nothrow_t, e.g. by value.
+ return this->emitInvalidNewDeleteExpr(E, E);
}
} else {
// Always invalid.
diff --git a/clang/lib/AST/ByteCode/Interp.cpp b/clang/lib/AST/ByteCode/Interp.cpp
index f885c0dc9cdb2..35a5eaa9abb67 100644
--- a/clang/lib/AST/ByteCode/Interp.cpp
+++ b/clang/lib/AST/ByteCode/Interp.cpp
@@ -2884,9 +2884,6 @@ bool CheckNewTypeMismatch(InterpState &S, CodePtr OpPC, const Expr *E,
if (!S.inConstantContext() && isConstexprUnknown(Ptr))
return false;
- if (!InvalidNewDeleteExpr(S, OpPC, E))
- return false;
-
const auto *NewExpr = cast<CXXNewExpr>(E);
const ASTContext &ASTCtx = S.getASTContext();
QualType StorageType = Ptr.getType();
@@ -2930,48 +2927,46 @@ bool CheckNewTypeMismatch(InterpState &S, CodePtr OpPC, const Expr *E,
bool InvalidNewDeleteExpr(InterpState &S, CodePtr OpPC, const Expr *E) {
assert(E);
+ const SourceInfo &Loc = S.Current->getSource(OpPC);
if (const auto *NewExpr = dyn_cast<CXXNewExpr>(E)) {
const FunctionDecl *OperatorNew = NewExpr->getOperatorNew();
- if (NewExpr->getNumPlacementArgs() > 0) {
- // This is allowed pre-C++26, but only an std function or if
- // [[msvc::constexpr]] was used.
- if (S.getLangOpts().CPlusPlus26 || S.Current->isStdFunction() ||
- S.Current->MSVCConstexprAllowed)
- return true;
-
- S.FFDiag(S.Current->getSource(OpPC), diag::note_constexpr_new_placement)
- << /*C++26 feature*/ 1 << E->getSourceRange();
- } else if (
- !OperatorNew
- ->isUsableAsGlobalAllocationFunctionInConstantEvaluation()) {
- S.FFDiag(S.Current->getSource(OpPC),
- diag::note_constexpr_new_non_replaceable)
- << isa<CXXMethodDecl>(OperatorNew) << OperatorNew;
- return false;
- } else if (!S.getLangOpts().CPlusPlus26 &&
- NewExpr->getNumPlacementArgs() == 1 &&
- !OperatorNew->isReservedGlobalPlacementOperator()) {
- if (!S.getLangOpts().CPlusPlus26) {
- S.FFDiag(S.Current->getSource(OpPC), diag::note_constexpr_new_placement)
- << /*Unsupported*/ 0 << E->getSourceRange();
- return false;
- }
- return true;
- }
- } else {
- const auto *DeleteExpr = cast<CXXDeleteExpr>(E);
- const FunctionDecl *OperatorDelete = DeleteExpr->getOperatorDelete();
- if (!OperatorDelete
- ->isUsableAsGlobalAllocationFunctionInConstantEvaluation()) {
- S.FFDiag(S.Current->getSource(OpPC),
- diag::note_constexpr_new_non_replaceable)
- << isa<CXXMethodDecl>(OperatorDelete) << OperatorDelete;
+ // The only new-placement list we support is (std::nothrow), and only for
+ // the replaceable global allocation functions.
+ bool IsNothrowForm = NewExpr->getNumPlacementArgs() == 1 &&
+ NewExpr->getPlacementArg(0)->getType()->isNothrowT();
+ if (NewExpr->getNumPlacementArgs() > 0 && !IsNothrowForm) {
+ S.FFDiag(Loc, diag::note_constexpr_new_placement)
+ << /*Unsupported*/ 0 << E->getSourceRange();
return false;
}
+
+ assert(
+ !OperatorNew->isUsableAsGlobalAllocationFunctionInConstantEvaluation());
+ S.FFDiag(Loc, diag::note_constexpr_new_non_replaceable)
+ << isa<CXXMethodDecl>(OperatorNew) << OperatorNew;
+ return false;
}
+ const auto *DeleteExpr = cast<CXXDeleteExpr>(E);
+ const FunctionDecl *OperatorDelete = DeleteExpr->getOperatorDelete();
+ assert(!OperatorDelete
+ ->isUsableAsGlobalAllocationFunctionInConstantEvaluation());
+ S.FFDiag(Loc, diag::note_constexpr_new_non_replaceable)
+ << isa<CXXMethodDecl>(OperatorDelete) << OperatorDelete;
+ return false;
+}
+
+bool CheckPlacementNew(InterpState &S, CodePtr OpPC, const Expr *E) {
+ // Placement new is allowed in C++26. Before that, it is only allowed in a
+ // std:: function or if [[msvc::constexpr]] was used.
+ if (S.getLangOpts().CPlusPlus26 || S.Current->isStdFunction() ||
+ S.Current->MSVCConstexprAllowed)
+ return true;
+
+ S.FFDiag(S.Current->getSource(OpPC), diag::note_constexpr_new_placement)
+ << /*C++26 feature*/ 1 << E->getSourceRange();
return false;
}
diff --git a/clang/lib/AST/ByteCode/Interp.h b/clang/lib/AST/ByteCode/Interp.h
index a106295022f9b..aed1e17a97479 100644
--- a/clang/lib/AST/ByteCode/Interp.h
+++ b/clang/lib/AST/ByteCode/Interp.h
@@ -4079,6 +4079,7 @@ bool CheckNewTypeMismatchArray(InterpState &S, CodePtr OpPC, const Expr *E) {
return CheckNewTypeMismatch(S, OpPC, E, static_cast<uint64_t>(Size));
}
bool InvalidNewDeleteExpr(InterpState &S, CodePtr OpPC, const Expr *E);
+bool CheckPlacementNew(InterpState &S, CodePtr OpPC, const Expr *E);
template <PrimType Name, class T = typename PrimConv<Name>::T>
inline bool BitCastPrim(InterpState &S, CodePtr OpPC, bool TargetIsUCharOrByte,
diff --git a/clang/lib/AST/ByteCode/Opcodes.td b/clang/lib/AST/ByteCode/Opcodes.td
index fcbd7461b6b5b..8b92654adfd6f 100644
--- a/clang/lib/AST/ByteCode/Opcodes.td
+++ b/clang/lib/AST/ByteCode/Opcodes.td
@@ -1025,6 +1025,7 @@ def CheckNewTypeMismatchArray : Opcode {
let HasGroup = 1;
}
def InvalidNewDeleteExpr : Opcode { let Args = [ArgExpr]; }
+def CheckPlacementNew : Opcode { let Args = [ArgExpr]; }
def IsConstantContext: SuccessOpcode;
def CheckAllocations : Opcode {
diff --git a/clang/lib/AST/ExprConstant.cpp b/clang/lib/AST/ExprConstant.cpp
index 72baa9406cdde..f7faffc767c1c 100644
--- a/clang/lib/AST/ExprConstant.cpp
+++ b/clang/lib/AST/ExprConstant.cpp
@@ -11018,23 +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).
- LValue Nothrow;
- if (!EvaluateLValue(E->getPlacementArg(0), Nothrow, Info))
- 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>())) {
@@ -11049,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
new file mode 100644
index 0000000000000..c2985ad582219
--- /dev/null
+++ b/clang/test/SemaCXX/new-nothrow-by-value.cpp
@@ -0,0 +1,29 @@
+// RUN: %clang_cc1 -std=c++20 -fsyntax-only -verify %s
+// RUN: %clang_cc1 -std=c++20 -fsyntax-only -verify %s -fexperimental-new-constant-interpreter
+
+// [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 {
+} nothrow;
+} // namespace std
+
+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);
+ 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