Case 09: C++ Vtable Change¶
| Field | Value |
|---|---|
| Verdict | ๐ด BREAKING |
| Category | Breaking |
| Platforms | Linux |
| Flags | ABI break, API break |
Detected ChangeKinds |
type_vtable_changed |
| Source files | examples/case09_cpp_vtable/ |
Category: C++ ABI | Verdict: ๐ด BREAKING
Verdict and consumer impact¶
Every Widget object carries a hidden vptr into a static array of virtual
function pointers. Old code calls widget->resize() via vtable slot 1.
After v2 inserts recolor() at slot 1, that same call site now dispatches
to recolor() instead โ silent wrong behavior with no crash and no error.
Old/new diff¶
| v1.h | v2.h |
|---|---|
virtual int draw(); |
virtual int draw(); |
virtual int resize(); |
virtual int recolor(); โ inserted |
virtual int resize(); |
abicheck command¶
g++ -shared -fPIC -g v1.cpp -o libwidget_v1.so
g++ -shared -fPIC -g v2.cpp -o libwidget_v2.so
abicheck compare libwidget_v1.so libwidget_v2.so
Expected abicheck finding¶
Verdict: BREAKING (exit 4)
- type_vtable_changed: vtable changed: Widget
(Widget::draw(), Widget::resize() -> Widget::draw(), Widget::recolor(), Widget::resize())
> Vtable slot reordering; virtual dispatch calls wrong method.
Affected symbols: make_widget
- vtable_slot_count_changed: Vtable for 'Widget' changed size: 32 -> 40 bytes (~2 -> ~3 virtual slots)
> A virtual method was added, removed, or reordered; existing binaries
dispatch through fixed vtable offsets and will call the wrong slot.
Minimum evidence¶
min_evidence: L1 โ DWARF's class/vtable metadata (DW_AT_vtable_elem_location
on each virtual member function) records each method's vtable slot for both
versions; -g alone is enough to detect the reordering, no public headers
required.
Why abicheck catches it¶
DWARF records the vtable slot index for every virtual method; abicheck
compares the ordered method-to-slot mapping between versions and flags any
class whose vtable layout changed โ plus a binary-only cross-check from the
_ZTV symbol's ELF size, which independently confirms the slot count grew.
Runtime failure demonstration¶
Severity: CRITICAL
Scenario: app compiled with v1's vtable layout (resize at slot 1)
calls resize(). With v2, recolor() is inserted at slot 1, so the app
calls the wrong method.
# Build v1 + app
g++ -shared -fPIC -g v1.cpp -o libwidget.so
g++ -g app.cpp -I. -L. -lwidget -Wl,-rpath,. -o app
./app
# โ draw() = 10 (expected 10)
# โ resize() = 20 (expected 20)
# Swap in v2 (no recompile)
g++ -shared -fPIC -g v2.cpp -o libwidget.so
./app
# โ draw() = 10 (expected 10)
# โ resize() = 99 (expected 20)
# โ WRONG RESULT: vtable layout changed
Why CRITICAL: the vtable in the app's compiled code indexes resize()
at slot 1. In v2 that slot now holds recolor() (returns 99). The wrong
method is called silently โ no crash, just completely wrong behavior.
Safe redesign¶
Only append new virtual methods โ never insert them in the middle of the vtable. Alternatively, use the non-virtual interface (NVI) pattern: keep a few virtual hooks and add non-virtual public methods that call them.
Real-world example: Qt's strict "no vtable reordering" rule is part of their documented binary-compatibility policy โ binary-compatible Qt releases never insert virtual methods in an existing class.
Cross-tool comparison¶
abidw --out-file v1.xml libwidget_v1.so
abidw --out-file v2.xml libwidget_v2.so
abidiff v1.xml v2.xml
echo "exit: $?" # โ 4
Note on abidiff 2.4.0: returns exit 4 even though this is a hard vtable incompatibility. abidiff's text output explicitly notes:
"note that this is an ABI incompatible change to the vtable of class Widget".
References¶
Source files¶
CMakeLists.txtapp.cppv1.cppv1.hv2.cppv2.h
See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.