Case 165: Polymorphic Type Without a Virtual Destructor (New Anti-Pattern)¶
| Field | Value |
|---|---|
| Verdict | ๐ก COMPATIBLE_WITH_RISK |
| Category | Risk |
| Platforms | Linux, macOS |
| Flags | Bad practice |
Detected ChangeKinds |
polymorphic_type_non_virtual_dtor |
| Source files | examples/case165_polymorphic_nonvirtual_dtor/ |
Category: API Design / Latent UB | Verdict: โ ๏ธ COMPATIBLE_WITH_RISK
Verdict and consumer impact¶
Nothing breaks today. v2 adds a brand-new polymorphic type, Exporter
(polymorphic because write() is virtual), whose destructor is not
virtual, plus a public factory (make_exporter()) that hands out owning
pointers. Existing binaries keep working unchanged โ the risk is planted
for the future: the moment a subclass of Exporter is returned from that
factory (e.g. a PdfExporter added in v3), delete through the base
pointer is undefined behavior ([expr.delete]/3) โ the derived destructor
is silently skipped, with no compiler or linker diagnostic on either side.
Old/new diff¶
| v1.h | v2.h |
|---|---|
class Renderer { virtual ~Renderer(); virtual void draw(int); }; |
(unchanged) |
| (no Exporter) | class Exporter { ~Exporter(); /* not virtual */ virtual void write(const char*); }; |
| (no factory) | Exporter* make_exporter(); |
abicheck command¶
g++ -shared -fPIC -g v1.cpp -o libv1.so
g++ -shared -fPIC -g v2.cpp -o libv2.so
abicheck compare libv1.so libv2.so --pattern-verdicts
Expected abicheck finding¶
Verdict: COMPATIBLE_WITH_RISK (exit 0)
Deployment Risk Changes:
- polymorphic_type_non_virtual_dtor: polymorphic type Exporter (used as
factory return) has a vtable but no virtual destructor โ delete through
base is UB
Additions:
- func_added: New public function: make_exporter
- type_added: New type: Exporter
Renderer (virtual dtor, present in both versions) is not flagged โ only
the newly introduced Exporter triggers the finding.
Minimum evidence¶
min_evidence: L1 โ DWARF alone carries enough to reconstruct the vtable
(via the mangled _ZTV... symbols and DW_AT_vtable_elem_location) and the
absence of a virtual destructor slot; no public headers are required. The
finding also needs --pattern-verdicts (ADR-027 opt-in anti-pattern
analysis) โ it is not emitted by a bare compare.
Why abicheck catches it¶
abicheck's ADR-027 single-snapshot anti-pattern analysis inspects each
type's vtable shape from DWARF: a type with a vtable (virtual functions)
but no destructor slot, used as a base class or returned by pointer from a
public factory, is flagged as polymorphic_type_non_virtual_dtor. It is
reported only for anti-patterns newly introduced on the new side โ
pre-existing debt already in v1 is not re-flagged on every run.
Runtime failure demonstration¶
Severity: INFORMATIONAL
Scenario: existing consumers keep working โ the risk is planted for the future, so there is no observable break to reproduce today.
# Build old library + app
g++ -shared -fPIC -g v1.cpp -o liblib.so
g++ -g app.cpp -L. -llib -Wl,-rpath,. -o app
./app
# โ frames drawn = 2 (expected 2)
# Swap in new library (no recompile)
g++ -shared -fPIC -g v2.cpp -o liblib.so
./app
# โ frames drawn = 2 (expected 2) โ no observable effect on existing binaries
The failure this case guards against only appears once a future release adds a subclass and returns it through the same factory:
class PdfExporter : public Exporter {
~PdfExporter() { fclose(f_); } // never runs!
};
// consumer:
Exporter* e = make_exporter(); // now returns a PdfExporter
delete e; // UB: ~Exporter() only, file leaks
Safe redesign¶
- Make the destructor virtual before the type ships:
virtual ~Exporter();(costless here โ the class already has a vtable). - If the type is not meant to be deleted polymorphically, make the
destructor
protected(and non-virtual) sodelete basedoes not compile. - Or return by
std::unique_ptr<Exporter, void(*)(Exporter*)>with a library-side deleter, so destruction always happens inside the library.
Real-world example: the C++ Core Guidelines encode this as
C.35: "A base class destructor should be either public and virtual, or
protected and non-virtual".
GCC/Clang ship -Wdelete-non-virtual-dtor (and -Wnon-virtual-dtor) because
this bug class is so common โ but those warnings only fire in the
consumer's translation unit, after the trap is already in the released
header.
References¶
- C++ Core Guidelines C.35
- KDE ABI Policy โ adding a virtual destructor later is itself BREAKING
- Scott Meyers, Effective C++, Item 7: "Declare destructors virtual in polymorphic base classes"
Source files¶
CMakeLists.txtapp.cppv1.cppv1.hv2.cppv2.h
See also: Examples overview ยท All COMPATIBLE_WITH_RISK cases ยท Category: Risk.