Skip to content

[C++20] Destroying delete and deleted destructors #118800

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions clang/docs/ReleaseNotes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -767,6 +767,9 @@ Bug Fixes in This Version
- Fixed a crash when passing the variable length array type to ``va_arg`` (#GH119360).
- Fixed a failed assertion when using ``__attribute__((noderef))`` on an
``_Atomic``-qualified type (#GH116124).
- No longer incorrectly diagnosing use of a deleted destructor when the
selected overload of ``operator delete`` for that type is a destroying delete
(#GH46818).
- No longer return ``false`` for ``noexcept`` expressions involving a
``delete`` which resolves to a destroying delete but the type of the object
being deleted has a potentially throwing destructor (#GH118660).
Expand Down
5 changes: 5 additions & 0 deletions clang/include/clang/AST/DeclCXX.h
Original file line number Diff line number Diff line change
Expand Up @@ -2855,6 +2855,11 @@ class CXXDestructorDecl : public CXXMethodDecl {
return getCanonicalDecl()->OperatorDeleteThisArg;
}

/// Will this destructor ever be called when considering which deallocation
/// function is associated with the destructor? Can optionally be passed an
/// 'operator delete' function declaration to test against specifically.
bool isCalledByDelete(const FunctionDecl *OpDel = nullptr) const;

CXXDestructorDecl *getCanonicalDecl() override {
return cast<CXXDestructorDecl>(FunctionDecl::getCanonicalDecl());
}
Expand Down
22 changes: 22 additions & 0 deletions clang/lib/AST/DeclCXX.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2969,6 +2969,28 @@ void CXXDestructorDecl::setOperatorDelete(FunctionDecl *OD, Expr *ThisArg) {
}
}

bool CXXDestructorDecl::isCalledByDelete(const FunctionDecl *OpDel) const {
// C++20 [expr.delete]p6: If the value of the operand of the delete-
// expression is not a null pointer value and the selected deallocation
// function (see below) is not a destroying operator delete, the delete-
// expression will invoke the destructor (if any) for the object or the
// elements of the array being deleted.
//
// This means we should not look at the destructor for a destroying
// delete operator, as that destructor is never called, unless the
// destructor is virtual (see [expr.delete]p8.1) because then the
// selected operator depends on the dynamic type of the pointer.
const FunctionDecl *SelectedOperatorDelete = OpDel ? OpDel : OperatorDelete;
if (!SelectedOperatorDelete)
return true;

if (!SelectedOperatorDelete->isDestroyingOperatorDelete())
return true;

// We have a destroying operator delete, so it depends on the dtor.
return isVirtual();
}

void CXXConversionDecl::anchor() {}

CXXConversionDecl *CXXConversionDecl::CreateDeserialized(ASTContext &C,
Expand Down
26 changes: 17 additions & 9 deletions clang/lib/Sema/SemaExceptionSpec.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1200,21 +1200,29 @@ CanThrowResult Sema::canThrow(const Stmt *S) {

case Expr::CXXDeleteExprClass: {
auto *DE = cast<CXXDeleteExpr>(S);
CanThrowResult CT;
CanThrowResult CT = CT_Cannot;
QualType DTy = DE->getDestroyedType();
if (DTy.isNull() || DTy->isDependentType()) {
CT = CT_Dependent;
} else {
// C++20 [expr.delete]p6: If the value of the operand of the delete-
// expression is not a null pointer value and the selected deallocation
// function (see below) is not a destroying operator delete, the delete-
// expression will invoke the destructor (if any) for the object or the
// elements of the array being deleted.
const FunctionDecl *OperatorDelete = DE->getOperatorDelete();
CT = canCalleeThrow(*this, DE, OperatorDelete);
if (!OperatorDelete->isDestroyingOperatorDelete()) {
if (const auto *RD = DTy->getAsCXXRecordDecl()) {
if (const CXXDestructorDecl *DD = RD->getDestructor())
CT = mergeCanThrow(CT, canCalleeThrow(*this, DE, DD));
}
if (CT == CT_Can)
return CT;
if (const auto *RD = DTy->getAsCXXRecordDecl()) {
if (const CXXDestructorDecl *DD = RD->getDestructor();
DD && DD->isCalledByDelete(OperatorDelete))
CT = canCalleeThrow(*this, DE, DD);
}

// We always look at the exception specification of the operator delete.
CT = mergeCanThrow(CT, canCalleeThrow(*this, DE, OperatorDelete));

// If we know we can throw, we're done.
if (CT == CT_Can)
return CT;
}
return mergeCanThrow(CT, canSubStmtsThrow(*this, DE));
}
Expand Down
18 changes: 11 additions & 7 deletions clang/lib/Sema/SemaExprCXX.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3792,13 +3792,16 @@ Sema::ActOnCXXDelete(SourceLocation StartLoc, bool UseGlobal,
.HasSizeT;
}

if (!PointeeRD->hasIrrelevantDestructor())
if (!PointeeRD->hasIrrelevantDestructor()) {
if (CXXDestructorDecl *Dtor = LookupDestructor(PointeeRD)) {
MarkFunctionReferenced(StartLoc,
const_cast<CXXDestructorDecl*>(Dtor));
if (DiagnoseUseOfDecl(Dtor, StartLoc))
return ExprError();
if (Dtor->isCalledByDelete(OperatorDelete)) {
MarkFunctionReferenced(StartLoc,
const_cast<CXXDestructorDecl *>(Dtor));
if (DiagnoseUseOfDecl(Dtor, StartLoc))
return ExprError();
}
}
}

CheckVirtualDtorCall(PointeeRD->getDestructor(), StartLoc,
/*IsDelete=*/true, /*CallCanBeVirtual=*/true,
Expand Down Expand Up @@ -3833,8 +3836,9 @@ Sema::ActOnCXXDelete(SourceLocation StartLoc, bool UseGlobal,
bool IsVirtualDelete = false;
if (PointeeRD) {
if (CXXDestructorDecl *Dtor = LookupDestructor(PointeeRD)) {
CheckDestructorAccess(Ex.get()->getExprLoc(), Dtor,
PDiag(diag::err_access_dtor) << PointeeElem);
if (Dtor->isCalledByDelete(OperatorDelete))
CheckDestructorAccess(Ex.get()->getExprLoc(), Dtor,
PDiag(diag::err_access_dtor) << PointeeElem);
IsVirtualDelete = Dtor->isVirtual();
}
}
Expand Down
49 changes: 47 additions & 2 deletions clang/test/CXX/expr/expr.unary/expr.delete/p10.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
// RUN: %clang_cc1 -std=c++1z -verify %s
// RUN: %clang_cc1 -std=c++20 -verify %s

using size_t = decltype(sizeof(0));
namespace std { enum class align_val_t : size_t {}; }
namespace std {
enum class align_val_t : size_t {};
struct destroying_delete_t {
explicit destroying_delete_t() = default;
};

inline constexpr destroying_delete_t destroying_delete{};
}

// Aligned version is preferred over unaligned version,
// unsized version is preferred over sized version.
Expand All @@ -23,3 +30,41 @@ struct alignas(Align) B {
};
void f(B<__STDCPP_DEFAULT_NEW_ALIGNMENT__> *p) { delete p; }
void f(B<__STDCPP_DEFAULT_NEW_ALIGNMENT__ * 2> *p) { delete p; } // expected-error {{deleted}}

// Ensure that a deleted destructor is acceptable when the selected overload
// for operator delete is a destroying delete. See the comments in GH118660.
struct S {
~S() = delete;
void operator delete(S *, std::destroying_delete_t) noexcept {}
};

struct T {
void operator delete(T *, std::destroying_delete_t) noexcept {}
private:
~T();
};

void foo(S *s, T *t) {
delete s; // Was rejected, is intended to be accepted.
delete t; // Was rejected, is intended to be accepted.
}

// However, if the destructor is virtual, then it has to be accessible because
// the behavior depends on which operator delete is selected and that is based
// on the dynamic type of the pointer.
struct U {
virtual ~U() = delete; // expected-note {{here}}
void operator delete(U *, std::destroying_delete_t) noexcept {}
};

struct V {
void operator delete(V *, std::destroying_delete_t) noexcept {}
private:
virtual ~V(); // expected-note {{here}}
};

void bar(U *u, V *v) {
// Both should be rejected because they have virtual destructors.
delete u; // expected-error {{attempt to use a deleted function}}
delete v; // expected-error {{calling a private destructor of class 'V'}}
}
10 changes: 5 additions & 5 deletions clang/test/SemaCXX/cxx2a-destroying-delete.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -146,12 +146,12 @@ namespace dtor_access {
struct S {
void operator delete(S *p, std::destroying_delete_t);
private:
~S(); // expected-note {{here}}
~S();
};

// FIXME: PR47474: GCC accepts this, and it seems somewhat reasonable to
// allow, even though [expr.delete]p12 says this is ill-formed.
void f() { delete new S; } // expected-error {{calling a private destructor}}
// C++20 [expr.delete]p12 says this is ill-formed, but GCC accepts and we
// filed CWG2889 to resolve in the same way.
void f() { delete new S; }

struct T {
void operator delete(T *, std::destroying_delete_t);
Expand All @@ -165,7 +165,7 @@ namespace dtor_access {
~U() override;
};

void g() { delete (T *)new U; } // expected-error {{calling a protected destructor}}
void g() { delete (T *)new U; } // expected-error {{calling a protected destructor of class 'T'}}
}

namespace delete_from_new {
Expand Down
17 changes: 16 additions & 1 deletion clang/test/SemaCXX/noexcept-destroying-delete.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
// RUN: %clang_cc1 -fsyntax-only -verify -fcxx-exceptions -Wno-unevaluated-expression -std=c++20 %s
// expected-no-diagnostics

namespace std {
struct destroying_delete_t {
Expand Down Expand Up @@ -31,3 +30,19 @@ ThrowingDestroyingDelete *pn = nullptr;
// noexcept should return false here because the destroying delete itself is a
// potentially throwing function.
static_assert(!noexcept(delete(pn)));


struct A {
virtual ~A(); // implicitly noexcept
};
struct B : A {
void operator delete(B *p, std::destroying_delete_t) { throw "oh no"; } // expected-warning {{'operator delete' has a non-throwing exception specification but can still throw}} \
expected-note {{deallocator has a implicit non-throwing exception specification}}
};
A *p = new B;

// Because the destructor for A is virtual, it is the only thing we consider
// when determining whether the delete expression can throw or not, despite the
// fact that the selected operator delete is a destroying delete. For virtual
// destructors, it's the dynamic type that matters.
static_assert(noexcept(delete p));
Loading