Skip to content

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.txt
  • app.cpp
  • copy_ctor_demo.cpp
  • v1.cpp
  • v1.hpp
  • v2.cpp
  • v2.hpp

See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.