[clang] [clang][bytecode] Implement __builtin_operator{new,delete} (PR #107672)
Timm Baeder via cfe-commits
cfe-commits at lists.llvm.org
Sat Sep 7 00:12:29 PDT 2024
https://github.com/tbaederr created https://github.com/llvm/llvm-project/pull/107672
None
>From fbdde33e018a721bab026dcffb4af22987cc7366 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Timm=20B=C3=A4der?= <tbaeder at redhat.com>
Date: Sat, 7 Sep 2024 09:10:00 +0200
Subject: [PATCH] [clang][bytecode] Implement __builtin_operator{new,delete}
---
clang/lib/AST/ByteCode/DynamicAllocator.cpp | 15 +-
clang/lib/AST/ByteCode/DynamicAllocator.h | 27 ++--
clang/lib/AST/ByteCode/Interp.cpp | 19 ++-
clang/lib/AST/ByteCode/Interp.h | 32 ++--
clang/lib/AST/ByteCode/InterpBuiltin.cpp | 160 ++++++++++++++++++++
clang/test/AST/ByteCode/new-delete.cpp | 112 ++++++++++++++
6 files changed, 334 insertions(+), 31 deletions(-)
diff --git a/clang/lib/AST/ByteCode/DynamicAllocator.cpp b/clang/lib/AST/ByteCode/DynamicAllocator.cpp
index a5159977407805..819fbdb8b070bf 100644
--- a/clang/lib/AST/ByteCode/DynamicAllocator.cpp
+++ b/clang/lib/AST/ByteCode/DynamicAllocator.cpp
@@ -40,27 +40,30 @@ void DynamicAllocator::cleanup() {
}
Block *DynamicAllocator::allocate(const Expr *Source, PrimType T,
- size_t NumElements, unsigned EvalID) {
+ size_t NumElements, unsigned EvalID,
+ Form AllocForm) {
// Create a new descriptor for an array of the specified size and
// element type.
const Descriptor *D = allocateDescriptor(
Source, T, Descriptor::InlineDescMD, NumElements, /*IsConst=*/false,
/*IsTemporary=*/false, /*IsMutable=*/false);
- return allocate(D, EvalID);
+ return allocate(D, EvalID, AllocForm);
}
Block *DynamicAllocator::allocate(const Descriptor *ElementDesc,
- size_t NumElements, unsigned EvalID) {
+ size_t NumElements, unsigned EvalID,
+ Form AllocForm) {
// Create a new descriptor for an array of the specified size and
// element type.
const Descriptor *D = allocateDescriptor(
ElementDesc->asExpr(), ElementDesc, Descriptor::InlineDescMD, NumElements,
/*IsConst=*/false, /*IsTemporary=*/false, /*IsMutable=*/false);
- return allocate(D, EvalID);
+ return allocate(D, EvalID, AllocForm);
}
-Block *DynamicAllocator::allocate(const Descriptor *D, unsigned EvalID) {
+Block *DynamicAllocator::allocate(const Descriptor *D, unsigned EvalID,
+ Form AllocForm) {
assert(D);
assert(D->asExpr());
@@ -84,7 +87,7 @@ Block *DynamicAllocator::allocate(const Descriptor *D, unsigned EvalID) {
It->second.Allocations.emplace_back(std::move(Memory));
else
AllocationSites.insert(
- {D->asExpr(), AllocationSite(std::move(Memory), D->isArray())});
+ {D->asExpr(), AllocationSite(std::move(Memory), AllocForm)});
return B;
}
diff --git a/clang/lib/AST/ByteCode/DynamicAllocator.h b/clang/lib/AST/ByteCode/DynamicAllocator.h
index a84600aa54cc56..1ed5dc843e4c8c 100644
--- a/clang/lib/AST/ByteCode/DynamicAllocator.h
+++ b/clang/lib/AST/ByteCode/DynamicAllocator.h
@@ -31,6 +31,14 @@ class InterpState;
/// For all array allocations, we need to allocate new Descriptor instances,
/// so the DynamicAllocator has a llvm::BumpPtrAllocator similar to Program.
class DynamicAllocator final {
+public:
+ enum class Form : uint8_t {
+ NonArray,
+ Array,
+ Operator,
+ };
+
+private:
struct Allocation {
std::unique_ptr<std::byte[]> Memory;
Allocation(std::unique_ptr<std::byte[]> Memory)
@@ -39,10 +47,10 @@ class DynamicAllocator final {
struct AllocationSite {
llvm::SmallVector<Allocation> Allocations;
- bool IsArrayAllocation = false;
+ Form AllocForm;
- AllocationSite(std::unique_ptr<std::byte[]> Memory, bool Array)
- : IsArrayAllocation(Array) {
+ AllocationSite(std::unique_ptr<std::byte[]> Memory, Form AllocForm)
+ : AllocForm(AllocForm) {
Allocations.push_back({std::move(Memory)});
}
@@ -58,12 +66,13 @@ class DynamicAllocator final {
unsigned getNumAllocations() const { return AllocationSites.size(); }
/// Allocate ONE element of the given descriptor.
- Block *allocate(const Descriptor *D, unsigned EvalID);
+ Block *allocate(const Descriptor *D, unsigned EvalID, Form AllocForm);
/// Allocate \p NumElements primitive elements of the given type.
Block *allocate(const Expr *Source, PrimType T, size_t NumElements,
- unsigned EvalID);
+ unsigned EvalID, Form AllocForm);
/// Allocate \p NumElements elements of the given descriptor.
- Block *allocate(const Descriptor *D, size_t NumElements, unsigned EvalID);
+ Block *allocate(const Descriptor *D, size_t NumElements, unsigned EvalID,
+ Form AllocForm);
/// Deallocate the given source+block combination.
/// Returns \c true if anything has been deallocatd, \c false otherwise.
@@ -72,10 +81,10 @@ class DynamicAllocator final {
/// Checks whether the allocation done at the given source is an array
/// allocation.
- bool isArrayAllocation(const Expr *Source) const {
+ std::optional<Form> getAllocationForm(const Expr *Source) const {
if (auto It = AllocationSites.find(Source); It != AllocationSites.end())
- return It->second.IsArrayAllocation;
- return false;
+ return It->second.AllocForm;
+ return std::nullopt;
}
/// Allocation site iterator.
diff --git a/clang/lib/AST/ByteCode/Interp.cpp b/clang/lib/AST/ByteCode/Interp.cpp
index 6777ac150abf4f..108cb2b12ac375 100644
--- a/clang/lib/AST/ByteCode/Interp.cpp
+++ b/clang/lib/AST/ByteCode/Interp.cpp
@@ -811,10 +811,11 @@ bool CheckDynamicMemoryAllocation(InterpState &S, CodePtr OpPC) {
return true;
}
-bool CheckNewDeleteForms(InterpState &S, CodePtr OpPC, bool NewWasArray,
- bool DeleteIsArray, const Descriptor *D,
+bool CheckNewDeleteForms(InterpState &S, CodePtr OpPC,
+ DynamicAllocator::Form AllocForm,
+ DynamicAllocator::Form DeleteForm, const Descriptor *D,
const Expr *NewExpr) {
- if (NewWasArray == DeleteIsArray)
+ if (AllocForm == DeleteForm)
return true;
QualType TypeToDiagnose;
@@ -831,7 +832,8 @@ bool CheckNewDeleteForms(InterpState &S, CodePtr OpPC, bool NewWasArray,
const SourceInfo &E = S.Current->getSource(OpPC);
S.FFDiag(E, diag::note_constexpr_new_delete_mismatch)
- << DeleteIsArray << 0 << TypeToDiagnose;
+ << static_cast<int>(DeleteForm) << static_cast<int>(AllocForm)
+ << TypeToDiagnose;
S.Note(NewExpr->getExprLoc(), diag::note_constexpr_dynamic_alloc_here)
<< NewExpr->getSourceRange();
return false;
@@ -839,7 +841,12 @@ bool CheckNewDeleteForms(InterpState &S, CodePtr OpPC, bool NewWasArray,
bool CheckDeleteSource(InterpState &S, CodePtr OpPC, const Expr *Source,
const Pointer &Ptr) {
- if (Source && isa<CXXNewExpr>(Source))
+ // The two sources we currently allow are new expressions and
+ // __builtin_operator_new calls.
+ if (isa_and_nonnull<CXXNewExpr>(Source))
+ return true;
+ if (const CallExpr *CE = dyn_cast_if_present<CallExpr>(Source);
+ CE && CE->getBuiltinCallee() == Builtin::BI__builtin_operator_new)
return true;
// Whatever this is, we didn't heap allocate it.
@@ -1165,6 +1172,8 @@ bool CallVirt(InterpState &S, CodePtr OpPC, const Function *Func,
bool CallBI(InterpState &S, CodePtr &PC, const Function *Func,
const CallExpr *CE) {
+ if (S.checkingPotentialConstantExpression())
+ return false;
auto NewFrame = std::make_unique<InterpFrame>(S, Func, PC);
InterpFrame *FrameBefore = S.Current;
diff --git a/clang/lib/AST/ByteCode/Interp.h b/clang/lib/AST/ByteCode/Interp.h
index be900769f25845..f83ac93b2b07d4 100644
--- a/clang/lib/AST/ByteCode/Interp.h
+++ b/clang/lib/AST/ByteCode/Interp.h
@@ -130,8 +130,9 @@ bool CheckNonNullArgs(InterpState &S, CodePtr OpPC, const Function *F,
bool CheckDynamicMemoryAllocation(InterpState &S, CodePtr OpPC);
/// Diagnose mismatched new[]/delete or new/delete[] pairs.
-bool CheckNewDeleteForms(InterpState &S, CodePtr OpPC, bool NewWasArray,
- bool DeleteIsArray, const Descriptor *D,
+bool CheckNewDeleteForms(InterpState &S, CodePtr OpPC,
+ DynamicAllocator::Form AllocForm,
+ DynamicAllocator::Form DeleteForm, const Descriptor *D,
const Expr *NewExpr);
/// Check the source of the pointer passed to delete/delete[] has actually
@@ -2793,10 +2794,11 @@ inline bool Alloc(InterpState &S, CodePtr OpPC, const Descriptor *Desc) {
return false;
DynamicAllocator &Allocator = S.getAllocator();
- Block *B = Allocator.allocate(Desc, S.Ctx.getEvalID());
+ Block *B = Allocator.allocate(Desc, S.Ctx.getEvalID(),
+ DynamicAllocator::Form::NonArray);
assert(B);
- S.Stk.push<Pointer>(B, sizeof(InlineDescriptor));
+ S.Stk.push<Pointer>(B);
return true;
}
@@ -2818,8 +2820,9 @@ inline bool AllocN(InterpState &S, CodePtr OpPC, PrimType T, const Expr *Source,
}
DynamicAllocator &Allocator = S.getAllocator();
- Block *B = Allocator.allocate(Source, T, static_cast<size_t>(NumElements),
- S.Ctx.getEvalID());
+ Block *B =
+ Allocator.allocate(Source, T, static_cast<size_t>(NumElements),
+ S.Ctx.getEvalID(), DynamicAllocator::Form::Array);
assert(B);
S.Stk.push<Pointer>(B, sizeof(InlineDescriptor));
@@ -2844,8 +2847,9 @@ inline bool AllocCN(InterpState &S, CodePtr OpPC, const Descriptor *ElementDesc,
}
DynamicAllocator &Allocator = S.getAllocator();
- Block *B = Allocator.allocate(ElementDesc, static_cast<size_t>(NumElements),
- S.Ctx.getEvalID());
+ Block *B =
+ Allocator.allocate(ElementDesc, static_cast<size_t>(NumElements),
+ S.Ctx.getEvalID(), DynamicAllocator::Form::Array);
assert(B);
S.Stk.push<Pointer>(B, sizeof(InlineDescriptor));
@@ -2890,8 +2894,9 @@ static inline bool Free(InterpState &S, CodePtr OpPC, bool DeleteIsArrayForm) {
return false;
DynamicAllocator &Allocator = S.getAllocator();
- bool WasArrayAlloc = Allocator.isArrayAllocation(Source);
const Descriptor *BlockDesc = BlockToDelete->getDescriptor();
+ std::optional<DynamicAllocator::Form> AllocForm =
+ Allocator.getAllocationForm(Source);
if (!Allocator.deallocate(Source, BlockToDelete, S)) {
// Nothing has been deallocated, this must be a double-delete.
@@ -2899,8 +2904,13 @@ static inline bool Free(InterpState &S, CodePtr OpPC, bool DeleteIsArrayForm) {
S.FFDiag(Loc, diag::note_constexpr_double_delete);
return false;
}
- return CheckNewDeleteForms(S, OpPC, WasArrayAlloc, DeleteIsArrayForm,
- BlockDesc, Source);
+
+ assert(AllocForm);
+ DynamicAllocator::Form DeleteForm = DeleteIsArrayForm
+ ? DynamicAllocator::Form::Array
+ : DynamicAllocator::Form::NonArray;
+ return CheckNewDeleteForms(S, OpPC, *AllocForm, DeleteForm, BlockDesc,
+ Source);
}
static inline bool IsConstantContext(InterpState &S, CodePtr OpPC) {
diff --git a/clang/lib/AST/ByteCode/InterpBuiltin.cpp b/clang/lib/AST/ByteCode/InterpBuiltin.cpp
index 81e49f203524b7..49fbaa3cbcb316 100644
--- a/clang/lib/AST/ByteCode/InterpBuiltin.cpp
+++ b/clang/lib/AST/ByteCode/InterpBuiltin.cpp
@@ -1244,6 +1244,156 @@ static bool interp__builtin_constant_p(InterpState &S, CodePtr OpPC,
return returnInt(false);
}
+static bool interp__builtin_operator_new(InterpState &S, CodePtr OpPC,
+ const InterpFrame *Frame,
+ const Function *Func,
+ const CallExpr *Call) {
+ // A call to __operator_new is only valid within std::allocate<>::allocate.
+ // Walk up the call stack to find the appropriate caller and get the
+ // element type from it.
+ QualType ElemType;
+
+ for (const InterpFrame *F = Frame; F; F = F->Caller) {
+ const Function *Func = F->getFunction();
+ if (!Func)
+ continue;
+ const auto *MD = dyn_cast_if_present<CXXMethodDecl>(Func->getDecl());
+ if (!MD)
+ continue;
+ const IdentifierInfo *FnII = MD->getIdentifier();
+ if (!FnII || !FnII->isStr("allocate"))
+ continue;
+
+ const auto *CTSD =
+ dyn_cast<ClassTemplateSpecializationDecl>(MD->getParent());
+ if (!CTSD)
+ continue;
+
+ const IdentifierInfo *ClassII = CTSD->getIdentifier();
+ const TemplateArgumentList &TAL = CTSD->getTemplateArgs();
+ if (CTSD->isInStdNamespace() && ClassII && ClassII->isStr("allocator") &&
+ TAL.size() >= 1 && TAL[0].getKind() == TemplateArgument::Type) {
+ ElemType = TAL[0].getAsType();
+ break;
+ }
+ }
+
+ if (ElemType.isNull()) {
+ S.FFDiag(Call, S.getLangOpts().CPlusPlus20
+ ? diag::note_constexpr_new_untyped
+ : diag::note_constexpr_new);
+ return false;
+ }
+
+ if (ElemType->isIncompleteType() || ElemType->isFunctionType()) {
+ S.FFDiag(Call, diag::note_constexpr_new_not_complete_object_type)
+ << (ElemType->isIncompleteType() ? 0 : 1) << ElemType;
+ return false;
+ }
+
+ APSInt Bytes = peekToAPSInt(S.Stk, *S.getContext().classify(Call->getArg(0)));
+ CharUnits ElemSize = S.getASTContext().getTypeSizeInChars(ElemType);
+ assert(!ElemSize.isZero());
+ // Divide the number of bytes by sizeof(ElemType), so we get the number of
+ // elements we should allocate.
+ APInt NumElems, Remainder;
+ APInt ElemSizeAP(Bytes.getBitWidth(), ElemSize.getQuantity());
+ APInt::udivrem(Bytes, ElemSizeAP, NumElems, Remainder);
+ if (Remainder != 0) {
+ // This likely indicates a bug in the implementation of 'std::allocator'.
+ S.FFDiag(Call, diag::note_constexpr_operator_new_bad_size)
+ << Bytes << APSInt(ElemSizeAP, true) << ElemType;
+ return false;
+ }
+
+ // FIXME: CheckArraySize for NumElems?
+
+ std::optional<PrimType> ElemT = S.getContext().classify(ElemType);
+ DynamicAllocator &Allocator = S.getAllocator();
+ if (ElemT) {
+ if (NumElems.ule(1)) {
+ const Descriptor *Desc =
+ S.P.createDescriptor(Call, *ElemT, Descriptor::InlineDescMD,
+ /*IsConst=*/false, /*IsTemporary=*/false,
+ /*IsMutable=*/false);
+ Block *B = Allocator.allocate(Desc, S.getContext().getEvalID(),
+ DynamicAllocator::Form::Operator);
+ assert(B);
+
+ S.Stk.push<Pointer>(B);
+ return true;
+ }
+ assert(NumElems.ugt(1));
+
+ Block *B =
+ Allocator.allocate(Call, *ElemT, NumElems.getZExtValue(),
+ S.Ctx.getEvalID(), DynamicAllocator::Form::Operator);
+ assert(B);
+ S.Stk.push<Pointer>(B);
+ return true;
+ }
+
+ assert(!ElemT);
+ // Structs etc.
+ const Descriptor *Desc = S.P.createDescriptor(
+ Call, ElemType.getTypePtr(),
+ NumElems.ule(1) ? std::nullopt : Descriptor::InlineDescMD,
+ /*IsConst=*/false, /*IsTemporary=*/false, /*IsMutable=*/false,
+ /*Init=*/nullptr);
+
+ if (NumElems.ule(1)) {
+ Block *B = Allocator.allocate(Desc, S.getContext().getEvalID(),
+ DynamicAllocator::Form::Operator);
+ assert(B);
+ S.Stk.push<Pointer>(B);
+ return true;
+ }
+
+ Block *B =
+ Allocator.allocate(Desc, NumElems.getZExtValue(), S.Ctx.getEvalID(),
+ DynamicAllocator::Form::Operator);
+ assert(B);
+ S.Stk.push<Pointer>(B);
+ return true;
+}
+
+static bool interp__builtin_operator_delete(InterpState &S, CodePtr OpPC,
+ const InterpFrame *Frame,
+ const Function *Func,
+ const CallExpr *Call) {
+ const Expr *Source = nullptr;
+ const Block *BlockToDelete = nullptr;
+
+ {
+ const Pointer &Ptr = S.Stk.peek<Pointer>();
+
+ if (Ptr.isZero()) {
+ S.CCEDiag(Call, diag::note_constexpr_deallocate_null);
+ return true;
+ }
+
+ Source = Ptr.getDeclDesc()->asExpr();
+ BlockToDelete = Ptr.block();
+ }
+ assert(BlockToDelete);
+
+ DynamicAllocator &Allocator = S.getAllocator();
+ const Descriptor *BlockDesc = BlockToDelete->getDescriptor();
+ std::optional<DynamicAllocator::Form> AllocForm =
+ Allocator.getAllocationForm(Source);
+
+ if (!Allocator.deallocate(Source, BlockToDelete, S)) {
+ // Nothing has been deallocated, this must be a double-delete.
+ const SourceInfo &Loc = S.Current->getSource(OpPC);
+ S.FFDiag(Loc, diag::note_constexpr_double_delete);
+ return false;
+ }
+ assert(AllocForm);
+
+ return CheckNewDeleteForms(
+ S, OpPC, *AllocForm, DynamicAllocator::Form::Operator, BlockDesc, Source);
+}
+
bool InterpretBuiltin(InterpState &S, CodePtr OpPC, const Function *F,
const CallExpr *Call) {
const InterpFrame *Frame = S.Current;
@@ -1597,6 +1747,16 @@ bool InterpretBuiltin(InterpState &S, CodePtr OpPC, const Function *F,
pushInteger(S, 0, Call->getType());
break;
+ case Builtin::BI__builtin_operator_new:
+ if (!interp__builtin_operator_new(S, OpPC, Frame, F, Call))
+ return false;
+ break;
+
+ case Builtin::BI__builtin_operator_delete:
+ if (!interp__builtin_operator_delete(S, OpPC, Frame, F, Call))
+ return false;
+ break;
+
default:
S.FFDiag(S.Current->getLocation(OpPC),
diag::note_invalid_subexpr_in_const_expr)
diff --git a/clang/test/AST/ByteCode/new-delete.cpp b/clang/test/AST/ByteCode/new-delete.cpp
index 145bb366710f9b..556efa65ae1181 100644
--- a/clang/test/AST/ByteCode/new-delete.cpp
+++ b/clang/test/AST/ByteCode/new-delete.cpp
@@ -586,6 +586,118 @@ constexpr void use_after_free_2() { // both-error {{never produces a constant ex
p->f(); // both-note {{member call on heap allocated object that has been deleted}}
}
+
+/// std::allocator definition
+namespace std {
+ using size_t = decltype(sizeof(0));
+ template<typename T> struct allocator {
+ constexpr T *allocate(size_t N) {
+ return (T*)__builtin_operator_new(sizeof(T) * N); // both-note 2{{allocation performed here}}
+ }
+ constexpr void deallocate(void *p) {
+ __builtin_operator_delete(p); // both-note 2{{std::allocator<...>::deallocate' used to delete pointer to object allocated with 'new'}} \
+ // both-note {{used to delete a null pointer}}
+ }
+ };
+}
+
+namespace OperatorNewDelete {
+
+ constexpr bool mismatched(int alloc_kind, int dealloc_kind) {
+ int *p;
+
+ if (alloc_kind == 0)
+ p = new int; // both-note {{allocation performed here}}
+ else if (alloc_kind == 1)
+ p = new int[1]; // both-note {{allocation performed here}}
+ else if (alloc_kind == 2)
+ p = std::allocator<int>().allocate(1);
+
+
+ if (dealloc_kind == 0)
+ delete p; // both-note {{'delete' used to delete pointer to object allocated with 'std::allocator<...>::allocate'}}
+ else if (dealloc_kind == 1)
+ delete[] p; // both-note {{'delete' used to delete pointer to object allocated with 'std::allocator<...>::allocate'}}
+ else if (dealloc_kind == 2)
+ std::allocator<int>().deallocate(p); // both-note 2{{in call to}}
+
+ return true;
+ }
+ static_assert(mismatched(0, 2)); // both-error {{constant expression}} \
+ // both-note {{in call to}}
+ static_assert(mismatched(1, 2)); // both-error {{constant expression}} \
+ // both-note {{in call to}}
+ static_assert(mismatched(2, 0)); // both-error {{constant expression}} \
+ // both-note {{in call}}
+ static_assert(mismatched(2, 1)); // both-error {{constant expression}} \
+ // both-note {{in call}}
+ static_assert(mismatched(2, 2));
+
+ constexpr bool zeroAlloc() {
+ int *F = std::allocator<int>().allocate(0);
+ std::allocator<int>().deallocate(F);
+ return true;
+ }
+ static_assert(zeroAlloc());
+
+ /// FIXME: This is broken in the current interpreter.
+ constexpr int arrayAlloc() {
+ int *F = std::allocator<int>().allocate(2);
+ F[0] = 10; // ref-note {{assignment to object outside its lifetime is not allowed in a constant expression}}
+ F[1] = 13;
+ int Res = F[1] + F[0];
+ std::allocator<int>().deallocate(F);
+ return Res;
+ }
+ static_assert(arrayAlloc() == 23); // ref-error {{not an integral constant expression}} \
+ // ref-note {{in call to}}
+
+ struct S {
+ int i;
+ constexpr S(int i) : i(i) {}
+ constexpr ~S() { }
+ };
+
+ /// FIXME: This is broken in the current interpreter.
+ constexpr bool structAlloc() {
+ S *s = std::allocator<S>().allocate(1);
+
+ s->i = 12; // ref-note {{assignment to object outside its lifetime is not allowed in a constant expression}}
+
+ bool Res = (s->i == 12);
+ std::allocator<S>().deallocate(s);
+
+ return Res;
+ }
+ static_assert(structAlloc()); // ref-error {{not an integral constant expression}} \
+ // ref-note {{in call to}}
+
+ constexpr bool structAllocArray() {
+ S *s = std::allocator<S>().allocate(9);
+
+ s[2].i = 12; // ref-note {{assignment to object outside its lifetime is not allowed in a constant expression}}
+ bool Res = (s[2].i == 12);
+ std::allocator<S>().deallocate(s);
+
+ return Res;
+ }
+ static_assert(structAllocArray()); // ref-error {{not an integral constant expression}} \
+ // ref-note {{in call to}}
+
+ constexpr bool alloc_from_user_code() {
+ void *p = __builtin_operator_new(sizeof(int)); // both-note {{cannot allocate untyped memory in a constant expression; use 'std::allocator<T>::allocate'}}
+ __builtin_operator_delete(p);
+ return true;
+ }
+ static_assert(alloc_from_user_code()); // both-error {{constant expression}} \
+ // both-note {{in call to}}
+
+
+ constexpr int no_deallocate_nullptr = (std::allocator<int>().deallocate(nullptr), 1); // both-error {{constant expression}} \
+ // both-note {{in call}}
+
+}
+
#else
/// Make sure we reject this prior to C++20
constexpr int a() { // both-error {{never produces a constant expression}}
More information about the cfe-commits
mailing list