Skip to content

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.