Skip to content

Commit 5ff7f47

Browse files
authored
[C++20] Destroying delete and deleted destructors (#118800)
When a destroying delete overload is selected, the destructor is not automatically called. Therefore, the destructor can be deleted without causing the program to be ill-formed. Fixes #46818
1 parent b4e17d4 commit 5ff7f47

File tree

8 files changed

+126
-24
lines changed

8 files changed

+126
-24
lines changed

clang/docs/ReleaseNotes.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -767,6 +767,9 @@ Bug Fixes in This Version
767767
- Fixed a crash when passing the variable length array type to ``va_arg`` (#GH119360).
768768
- Fixed a failed assertion when using ``__attribute__((noderef))`` on an
769769
``_Atomic``-qualified type (#GH116124).
770+
- No longer incorrectly diagnosing use of a deleted destructor when the
771+
selected overload of ``operator delete`` for that type is a destroying delete
772+
(#GH46818).
770773
- No longer return ``false`` for ``noexcept`` expressions involving a
771774
``delete`` which resolves to a destroying delete but the type of the object
772775
being deleted has a potentially throwing destructor (#GH118660).

clang/include/clang/AST/DeclCXX.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2855,6 +2855,11 @@ class CXXDestructorDecl : public CXXMethodDecl {
28552855
return getCanonicalDecl()->OperatorDeleteThisArg;
28562856
}
28572857

2858+
/// Will this destructor ever be called when considering which deallocation
2859+
/// function is associated with the destructor? Can optionally be passed an
2860+
/// 'operator delete' function declaration to test against specifically.
2861+
bool isCalledByDelete(const FunctionDecl *OpDel = nullptr) const;
2862+
28582863
CXXDestructorDecl *getCanonicalDecl() override {
28592864
return cast<CXXDestructorDecl>(FunctionDecl::getCanonicalDecl());
28602865
}

clang/lib/AST/DeclCXX.cpp

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2969,6 +2969,28 @@ void CXXDestructorDecl::setOperatorDelete(FunctionDecl *OD, Expr *ThisArg) {
29692969
}
29702970
}
29712971

2972+
bool CXXDestructorDecl::isCalledByDelete(const FunctionDecl *OpDel) const {
2973+
// C++20 [expr.delete]p6: If the value of the operand of the delete-
2974+
// expression is not a null pointer value and the selected deallocation
2975+
// function (see below) is not a destroying operator delete, the delete-
2976+
// expression will invoke the destructor (if any) for the object or the
2977+
// elements of the array being deleted.
2978+
//
2979+
// This means we should not look at the destructor for a destroying
2980+
// delete operator, as that destructor is never called, unless the
2981+
// destructor is virtual (see [expr.delete]p8.1) because then the
2982+
// selected operator depends on the dynamic type of the pointer.
2983+
const FunctionDecl *SelectedOperatorDelete = OpDel ? OpDel : OperatorDelete;
2984+
if (!SelectedOperatorDelete)
2985+
return true;
2986+
2987+
if (!SelectedOperatorDelete->isDestroyingOperatorDelete())
2988+
return true;
2989+
2990+
// We have a destroying operator delete, so it depends on the dtor.
2991+
return isVirtual();
2992+
}
2993+
29722994
void CXXConversionDecl::anchor() {}
29732995

29742996
CXXConversionDecl *CXXConversionDecl::CreateDeserialized(ASTContext &C,

clang/lib/Sema/SemaExceptionSpec.cpp

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1200,21 +1200,29 @@ CanThrowResult Sema::canThrow(const Stmt *S) {
12001200

12011201
case Expr::CXXDeleteExprClass: {
12021202
auto *DE = cast<CXXDeleteExpr>(S);
1203-
CanThrowResult CT;
1203+
CanThrowResult CT = CT_Cannot;
12041204
QualType DTy = DE->getDestroyedType();
12051205
if (DTy.isNull() || DTy->isDependentType()) {
12061206
CT = CT_Dependent;
12071207
} else {
1208+
// C++20 [expr.delete]p6: If the value of the operand of the delete-
1209+
// expression is not a null pointer value and the selected deallocation
1210+
// function (see below) is not a destroying operator delete, the delete-
1211+
// expression will invoke the destructor (if any) for the object or the
1212+
// elements of the array being deleted.
12081213
const FunctionDecl *OperatorDelete = DE->getOperatorDelete();
1209-
CT = canCalleeThrow(*this, DE, OperatorDelete);
1210-
if (!OperatorDelete->isDestroyingOperatorDelete()) {
1211-
if (const auto *RD = DTy->getAsCXXRecordDecl()) {
1212-
if (const CXXDestructorDecl *DD = RD->getDestructor())
1213-
CT = mergeCanThrow(CT, canCalleeThrow(*this, DE, DD));
1214-
}
1215-
if (CT == CT_Can)
1216-
return CT;
1214+
if (const auto *RD = DTy->getAsCXXRecordDecl()) {
1215+
if (const CXXDestructorDecl *DD = RD->getDestructor();
1216+
DD && DD->isCalledByDelete(OperatorDelete))
1217+
CT = canCalleeThrow(*this, DE, DD);
12171218
}
1219+
1220+
// We always look at the exception specification of the operator delete.
1221+
CT = mergeCanThrow(CT, canCalleeThrow(*this, DE, OperatorDelete));
1222+
1223+
// If we know we can throw, we're done.
1224+
if (CT == CT_Can)
1225+
return CT;
12181226
}
12191227
return mergeCanThrow(CT, canSubStmtsThrow(*this, DE));
12201228
}

clang/lib/Sema/SemaExprCXX.cpp

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3792,13 +3792,16 @@ Sema::ActOnCXXDelete(SourceLocation StartLoc, bool UseGlobal,
37923792
.HasSizeT;
37933793
}
37943794

3795-
if (!PointeeRD->hasIrrelevantDestructor())
3795+
if (!PointeeRD->hasIrrelevantDestructor()) {
37963796
if (CXXDestructorDecl *Dtor = LookupDestructor(PointeeRD)) {
3797-
MarkFunctionReferenced(StartLoc,
3798-
const_cast<CXXDestructorDecl*>(Dtor));
3799-
if (DiagnoseUseOfDecl(Dtor, StartLoc))
3800-
return ExprError();
3797+
if (Dtor->isCalledByDelete(OperatorDelete)) {
3798+
MarkFunctionReferenced(StartLoc,
3799+
const_cast<CXXDestructorDecl *>(Dtor));
3800+
if (DiagnoseUseOfDecl(Dtor, StartLoc))
3801+
return ExprError();
3802+
}
38013803
}
3804+
}
38023805

38033806
CheckVirtualDtorCall(PointeeRD->getDestructor(), StartLoc,
38043807
/*IsDelete=*/true, /*CallCanBeVirtual=*/true,
@@ -3833,8 +3836,9 @@ Sema::ActOnCXXDelete(SourceLocation StartLoc, bool UseGlobal,
38333836
bool IsVirtualDelete = false;
38343837
if (PointeeRD) {
38353838
if (CXXDestructorDecl *Dtor = LookupDestructor(PointeeRD)) {
3836-
CheckDestructorAccess(Ex.get()->getExprLoc(), Dtor,
3837-
PDiag(diag::err_access_dtor) << PointeeElem);
3839+
if (Dtor->isCalledByDelete(OperatorDelete))
3840+
CheckDestructorAccess(Ex.get()->getExprLoc(), Dtor,
3841+
PDiag(diag::err_access_dtor) << PointeeElem);
38383842
IsVirtualDelete = Dtor->isVirtual();
38393843
}
38403844
}

clang/test/CXX/expr/expr.unary/expr.delete/p10.cpp

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
1-
// RUN: %clang_cc1 -std=c++1z -verify %s
1+
// RUN: %clang_cc1 -std=c++20 -verify %s
22

33
using size_t = decltype(sizeof(0));
4-
namespace std { enum class align_val_t : size_t {}; }
4+
namespace std {
5+
enum class align_val_t : size_t {};
6+
struct destroying_delete_t {
7+
explicit destroying_delete_t() = default;
8+
};
9+
10+
inline constexpr destroying_delete_t destroying_delete{};
11+
}
512

613
// Aligned version is preferred over unaligned version,
714
// unsized version is preferred over sized version.
@@ -23,3 +30,41 @@ struct alignas(Align) B {
2330
};
2431
void f(B<__STDCPP_DEFAULT_NEW_ALIGNMENT__> *p) { delete p; }
2532
void f(B<__STDCPP_DEFAULT_NEW_ALIGNMENT__ * 2> *p) { delete p; } // expected-error {{deleted}}
33+
34+
// Ensure that a deleted destructor is acceptable when the selected overload
35+
// for operator delete is a destroying delete. See the comments in GH118660.
36+
struct S {
37+
~S() = delete;
38+
void operator delete(S *, std::destroying_delete_t) noexcept {}
39+
};
40+
41+
struct T {
42+
void operator delete(T *, std::destroying_delete_t) noexcept {}
43+
private:
44+
~T();
45+
};
46+
47+
void foo(S *s, T *t) {
48+
delete s; // Was rejected, is intended to be accepted.
49+
delete t; // Was rejected, is intended to be accepted.
50+
}
51+
52+
// However, if the destructor is virtual, then it has to be accessible because
53+
// the behavior depends on which operator delete is selected and that is based
54+
// on the dynamic type of the pointer.
55+
struct U {
56+
virtual ~U() = delete; // expected-note {{here}}
57+
void operator delete(U *, std::destroying_delete_t) noexcept {}
58+
};
59+
60+
struct V {
61+
void operator delete(V *, std::destroying_delete_t) noexcept {}
62+
private:
63+
virtual ~V(); // expected-note {{here}}
64+
};
65+
66+
void bar(U *u, V *v) {
67+
// Both should be rejected because they have virtual destructors.
68+
delete u; // expected-error {{attempt to use a deleted function}}
69+
delete v; // expected-error {{calling a private destructor of class 'V'}}
70+
}

clang/test/SemaCXX/cxx2a-destroying-delete.cpp

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -146,12 +146,12 @@ namespace dtor_access {
146146
struct S {
147147
void operator delete(S *p, std::destroying_delete_t);
148148
private:
149-
~S(); // expected-note {{here}}
149+
~S();
150150
};
151151

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

156156
struct T {
157157
void operator delete(T *, std::destroying_delete_t);
@@ -165,7 +165,7 @@ namespace dtor_access {
165165
~U() override;
166166
};
167167

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

171171
namespace delete_from_new {

clang/test/SemaCXX/noexcept-destroying-delete.cpp

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
// RUN: %clang_cc1 -fsyntax-only -verify -fcxx-exceptions -Wno-unevaluated-expression -std=c++20 %s
2-
// expected-no-diagnostics
32

43
namespace std {
54
struct destroying_delete_t {
@@ -31,3 +30,19 @@ ThrowingDestroyingDelete *pn = nullptr;
3130
// noexcept should return false here because the destroying delete itself is a
3231
// potentially throwing function.
3332
static_assert(!noexcept(delete(pn)));
33+
34+
35+
struct A {
36+
virtual ~A(); // implicitly noexcept
37+
};
38+
struct B : A {
39+
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}} \
40+
expected-note {{deallocator has a implicit non-throwing exception specification}}
41+
};
42+
A *p = new B;
43+
44+
// Because the destructor for A is virtual, it is the only thing we consider
45+
// when determining whether the delete expression can throw or not, despite the
46+
// fact that the selected operator delete is a destroying delete. For virtual
47+
// destructors, it's the dynamic type that matters.
48+
static_assert(noexcept(delete p));

0 commit comments

Comments
 (0)