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 | browse on GitHub |
Category: C++ ABI | Verdict: ๐ก ABI CHANGE (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".
What breaks¶
Every Widget object contains a hidden vptr (pointer to the vtable โ a static array of function pointers).
Old code calls widget->resize() via vtable slot 1. After v2 inserts recolor() at
slot 1, that same call dispatches to recolor() instead โ silent wrong behavior or
a crash.
Why abidiff catches it¶
Reports the vtable offset of method virtual int Widget::resize() changed from 1 to 2
and labels it "ABI incompatible change to the vtable."
Code diff¶
| v1.cpp | v2.cpp |
|---|---|
virtual int draw(); |
virtual int draw(); |
virtual int resize(); |
virtual int recolor(); โ inserted |
virtual int resize(); |
Reproduce manually¶
g++ -shared -fPIC -g v1.cpp -o libwidget_v1.so
g++ -shared -fPIC -g v2.cpp -o libwidget_v2.so
abidw --out-file v1.xml libwidget_v1.so
abidw --out-file v2.xml libwidget_v2.so
abidiff v1.xml v2.xml
echo "exit: $?" # โ 4
How to fix¶
Only append new virtual methods โ never insert them in the middle of the vtable. Alternatively, use the non-virtual interface (NVI) pattern: make only a few virtual hooks, add non-virtual public methods that call them.
Real-world example¶
Qt's strict "no vtable reordering" rule is documented in their ABI compatibility policy. Binary-compatible Qt releases never insert virtual methods.
Real Failure Demo¶
Severity: CRITICAL
Scenario: app compiled with v1 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: dispatched to recolor()!
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.
References¶
Source files¶
See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.