Case 38: Virtual Method Changes¶
| Field | Value |
|---|---|
| Verdict | ๐ด BREAKING |
| Category | Breaking |
| Platforms | Linux, macOS, Windows |
| Flags | ABI break, API break |
Detected ChangeKinds |
type_vtable_changed |
| Source files | examples/case38_virtual_methods/ |
Category: C++ Virtual / Deleted | Verdict: ๐ด BREAKING
Verdict and consumer impact¶
Four independent changes land on Processor in one release: transform()
becomes virtual, validate() loses virtual, execute() becomes pure
virtual, and the copy constructor is = deleted. Each one invalidates an
assumption baked into any binary compiled against v1 โ vtable slot
indices, the presence of a concrete execute() implementation, and the
copy-constructor symbol itself. Any consumer that dispatches through a
Processor&, instantiates Processor directly, or copies one is broken
without recompilation.
Old/new diff¶
| v1.hpp | v2.hpp |
|---|---|
void transform(int data); |
virtual void transform(int data); (became virtual) |
virtual void validate(int data); |
void validate(int data); (lost virtual) |
virtual void execute(); |
virtual void execute() = 0; (became pure virtual) |
Processor(const Processor &other); |
Processor(const Processor &other) = delete; |
abicheck command¶
g++ -shared -fPIC -g v1.cpp -o libfoo_v1.so
g++ -shared -fPIC -g v2.cpp -o libfoo_v2.so
abicheck compare libfoo_v1.so libfoo_v2.so
Expected abicheck finding¶
Verdict: BREAKING (exit 4)
- type_vtable_changed: vtable changed: Processor
(Processor::validate(int), Processor::execute(), Processor::~Processor()
-> Processor::transform(int), Processor::execute(), Processor::~Processor())
> Vtable slot reordering; virtual dispatch calls wrong method.
- func_removed_elf_only: Elf_only function removed: Processor::execute()
> Exported function symbol removed from the binary; old binaries that
link or dlsym() it can fail even without header evidence.
- func_removed_elf_only: Elf_only function removed: Processor::validate(int)
- func_removed_elf_only: Elf_only function removed:
Processor::Processor(Processor const&)
- var_removed: Public variable removed: vtable for Processor
> Old binaries reference a global variable that no longer exists;
link or load failure.
(12 breaking findings total; abicheck flags 3 of them as RTTI-symbol churn
typical of a missing -fvisibility=hidden rather than distinct public-API
breaks โ 9 are genuine public-surface breaks, matching the four source
changes above.)
Minimum evidence¶
min_evidence: L1 โ DWARF's vtable member list (DW_AT_vtable_elem_location
per virtual method) plus the exported-symbol table are enough to detect all
four changes; no public headers required. Unlike case37, Processor
already has out-of-line virtual method definitions in v1.cpp, so GCC emits
a complete class DIE by default โ no extra debug flag needed here.
Why abicheck catches it¶
DWARF records each virtual method's vtable slot via
DW_AT_vtable_elem_location; abicheck diffs the ordered vtable-slot list
per class directly from debug info. The symbol-table view independently
confirms execute(), validate(), and the copy constructor no longer
exist as exported functions, and that the vtable/typeinfo/typeinfo-name
globals for Processor changed.
Runtime failure demonstration¶
Severity: CRITICAL
Scenario: app instantiates Processor directly (legal in v1, where
execute() has a concrete body) and dispatches through a Processor&,
then the library is swapped for v2 without recompiling the app.
# Build old library + app
g++ -shared -fPIC -g v1.cpp -o libprocessor.so
g++ -g app.cpp -I. -L. -lprocessor -Wl,-rpath,. -o app
./app
# โ MyProcessor::execute() called
# โ exit 0
# Swap in new library (no recompile)
g++ -shared -fPIC -g v2.cpp -o libprocessor.so
./app
# โ pure virtual method called
# โ terminate called without an active exception
# โ Aborted
# โ exit 134
The deleted-copy-constructor change is exercised independently
(copy_ctor_demo.cpp, built against v1 where the copy ctor exists):
g++ -g copy_ctor_demo.cpp -I. -L. -lprocessor -Wl,-rpath,. -o copy_ctor_demo
./copy_ctor_demo # (built against v1) โ "Copy created successfully", exit 0
# Swap to v2 (no recompile):
g++ -shared -fPIC -g v2.cpp -o libprocessor.so
./copy_ctor_demo
# โ symbol lookup error: undefined symbol: _ZN9ProcessorC1ERKS_
# โ exit 127
Why CRITICAL: in v2, Processor::execute()'s vtable slot points to
__cxa_pure_virtual, so any v1-compiled object built as a bare Processor
aborts the process the moment execute() is dispatched. The deleted copy
constructor separately removes a symbol outright, producing a hard
load-time/link-time failure for any code path that copies a Processor.
Safe redesign¶
Never change the virtual-ness of an existing method in a stable ABI. To add new virtual behavior, append a new virtual method (never reorder existing ones) and bump the SONAME; a pure-virtual addition is a source break for every existing concrete subclass and needs a major version bump. If a constructor must become uncopyable, do it in a new major version, not a patch release โ the old symbol has to keep existing for one deprecation cycle.
Real-world example: Qt and other GUI toolkits carefully preserve virtual-method order and slot count across minor releases specifically because plugins compiled against an older header still dispatch through the vtable at runtime; a reordered vtable silently breaks every third-party plugin.
Cross-tool comparison¶
abidw --out-file v1.xml libv1.so
abidw --out-file v2.xml libv2.so
abidiff v1.xml v2.xml
echo "exit: $?" # โ 12 (ABI change + breaking)
References¶
Source files¶
CMakeLists.txtapp.cppcopy_ctor_demo.cppv1.cppv1.hppv2.cppv2.hpp
See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.