Skip to content

Commit 212e23e

Browse files
dkorpelclaude
andcommitted
Fix #22709 - Use of extern(C++) destructor is hidden
Previously, the implicit override of a base class destructor for C++ classes was shallow, so with an empty class B sitting inbetween A and C you would get a vtbl = [A.~this, C.~this], where dmd considers A.~this hidden by C.~this. Now, C finds the vtable index of A resulting in vtbl = [C.~this]. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent c13781c commit 212e23e

4 files changed

Lines changed: 94 additions & 2 deletions

File tree

compiler/src/dmd/dsymbolsem.d

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4502,10 +4502,20 @@ private extern(C++) final class DsymbolSemanticVisitor : Visitor
45024502
if (auto cldec = ad.isClassDeclaration())
45034503
{
45044504
assert (cldec.cppDtorVtblIndex == -1); // double-call check already by dd.type
4505-
if (cldec.baseClass && cldec.baseClass.cppDtorVtblIndex != -1)
4505+
// Walk up the base chain: an intermediate class may have no explicit
4506+
// dtor (cppDtorVtblIndex == -1) yet still inherit a dtor vtbl slot.
4507+
// https://github.com/dlang/dmd/issues/22709
4508+
int inheritedDtorVtblIndex = -1;
4509+
for (auto base = cldec.baseClass; base; base = base.baseClass)
4510+
if (base.cppDtorVtblIndex != -1)
4511+
{
4512+
inheritedDtorVtblIndex = base.cppDtorVtblIndex;
4513+
break;
4514+
}
4515+
if (inheritedDtorVtblIndex != -1)
45064516
{
45074517
// override the base virtual
4508-
cldec.cppDtorVtblIndex = cldec.baseClass.cppDtorVtblIndex;
4518+
cldec.cppDtorVtblIndex = inheritedDtorVtblIndex;
45094519
}
45104520
else if (!dd.isFinal())
45114521
{
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// https://github.com/dlang/dmd/issues/22709
2+
// extern(C++) destructor in base class should not be flagged as hidden
3+
4+
extern(C++):
5+
class A
6+
{
7+
~this();
8+
}
9+
class B : A
10+
{
11+
}
12+
class C : B
13+
{
14+
~this();
15+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// https://github.com/dlang/dmd/issues/22709
2+
// Virtual dtor dispatch through an intermediate class with no explicit dtor.
3+
// Before the fix, C's vtbl had a stale entry for A's dtor at slot 0, causing
4+
// virtual dtor dispatch through A* to call A's dtor instead of C's.
5+
// EXTRA_CPP_SOURCES: cpp_dtor.cpp
6+
7+
extern(C) __gshared int aDestroyed;
8+
extern(C) __gshared int cDestroyed;
9+
10+
extern(C++) void runCPPTests();
11+
12+
extern(C++):
13+
14+
class A
15+
{
16+
~this() { aDestroyed = 1; }
17+
}
18+
19+
class B : A
20+
{
21+
}
22+
23+
class C : B
24+
{
25+
~this() { cDestroyed = 1; }
26+
}
27+
28+
// D-side factory: C++ calls this to get a C object typed as A*
29+
A makeC() { return new C; }
30+
31+
extern(D) void main()
32+
{
33+
runCPPTests();
34+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// https://github.com/dlang/dmd/issues/22709
2+
// C++ side: verify virtual dtor dispatch calls C's dtor (not A's) when
3+
// destroying a C object through an A*.
4+
#include <assert.h>
5+
6+
extern "C" int aDestroyed;
7+
extern "C" int cDestroyed;
8+
9+
// Forward declaration matching D's extern(C++) class A
10+
class A {
11+
public:
12+
virtual ~A();
13+
};
14+
15+
// D-side factory
16+
extern "C++" A* makeC();
17+
18+
void runCPPTests()
19+
{
20+
A* obj = makeC();
21+
22+
// Invoke the virtual destructor without freeing memory.
23+
// obj->~A() dispatches virtually (calls C's dtor) on all ABIs,
24+
// and does NOT call operator delete, so D-allocated memory is safe.
25+
aDestroyed = 0;
26+
cDestroyed = 0;
27+
obj->~A();
28+
29+
// C's destructor must be dispatched, not A's
30+
assert(cDestroyed);
31+
// A's destructor must be chained from C's aggregate dtor
32+
assert(aDestroyed);
33+
}

0 commit comments

Comments
 (0)