Skip to content

Case 108: task Class Removed (historical ABI break โ€” vtable angle)

Field Value
Verdict ๐Ÿ”ด BREAKING
Category Breaking
Platforms Linux, macOS, Windows
Flags ABI break, API break
Detected ChangeKinds func_removed, type_removed
Source files examples/case108_task_class_removed/

Category: Polymorphic Class Removal | Verdict: ๐Ÿ”ด BREAKING

Verdict and consumer impact

An entire publicly-derivable polymorphic base class is removed. Every user subclass that overrode the virtual execute() becomes a vtable error at link/load time: the typeinfo symbol for the v1 base is gone, RTTI fails, and delete on a polymorphic pointer to the removed base invokes UB. This is more severe than a plain class removal (case107) because the base class was a derivation point โ€” user code embedded the v1 vtable layout into every derived class's own vtable, and the RTTI strings crossed DSO boundaries.

Old/new diff

v1.h v2.h
class task { public: task(); virtual ~task(); virtual task* execute() = 0; ... }; (removed)
task* mylib_spawn_dummy(); (removed)
โ€” class task_group { public: void run(task_fn); void wait(); };

abicheck command

g++ -std=c++17 -shared -fPIC -g v1.cpp -o libfoo_v1.so
g++ -std=c++17 -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_removed: Type removed: mylib::task
  > Old code references a type that no longer exists; compilation or link
    failure.
- type_removed: Type removed: mylib::dummy_task
- func_removed_elf_only: Elf_only function removed: mylib::mylib_spawn_dummy()
  > (and 6 more: task ctor/dtor/set_ref_count/decrement_ref_count)
- var_removed: Public variable removed: vtable for mylib::task
- var_removed: Public variable removed: typeinfo for mylib::task
- var_removed: Public variable removed: typeinfo name for mylib::task

Additions:
- type_added: New type: mylib::task_group
- func_added: New public function: mylib::task_group::run/wait/task_group()

10 genuine public-surface breaking findings (3 of 13 raw findings are
internal RTTI/dependency churn, annotated separately by the reporter).

Minimum evidence

min_evidence: L1 โ€” task's ctor/dtor/methods, its vtable, and its typeinfo symbols are all emitted from v1.cpp (which instantiates a dummy_task subclass), so DWARF + the dynamic symbol table are enough; no public headers are required.

Why abicheck catches it

DWARF exposes the class hierarchy and virtual member layout directly, and the ELF symbol table carries the mangled vtable (_ZTV7mylib4task) and typeinfo (_ZTI7mylib4task) symbols alongside the ordinary member functions. When the whole class disappears in v2, abicheck's DWARF/ELF diff reports the type, its members, and its vtable/typeinfo variables as removed together โ€” the full surface of what "removing a polymorphic base" actually costs a consumer.

Runtime failure demonstration

Severity: BREAKING

Scenario: compile app against v1, swap in v2 .so without recompile.

# Build old library + app
g++ -shared -fPIC -g v1.cpp -o libfoo.so
g++ -g app.cpp -L. -lfoo -Wl,-rpath,. -o app
./app
# โ†’ ref_count after dec = 2 (expect 2)

# Swap in new library (no recompile)
g++ -shared -fPIC -g v2.cpp -o libfoo.so
./app
# โ†’ ./app: symbol lookup error: ./app: undefined symbol:
#   _ZN5mylib17mylib_spawn_dummyEv

# Source rebuild against the v2 header also fails: the base class and
# factory are both gone.
# โ†’ error: 'task' is not a member of 'mylib'
# โ†’ error: 'mylib_spawn_dummy' is not a member of 'mylib'

Why BREAKING: app calls the factory mylib_spawn_dummy() to obtain a polymorphic task*; that symbol โ€” along with the vtable and typeinfo it depends on โ€” no longer exists in v2, so the dynamic linker refuses to resolve it before main() even runs.

Safe redesign

A polymorphic base class is the most expensive thing to remove. The safe migration is: 1. Keep the old base class header in a legacy/ subdir for one release. 2. Provide an adapter that wraps the new API in the old interface. 3. Bump SONAME on removal. 4. Document the equivalence map (which task workflow maps to which task_group call).

Real-world example: classic TBB's tbb::task low-level API was the recommended way to write parallel algorithms before parallel_invoke / task_group. It was fully removed in oneTBB 2021.1 alongside task_scheduler_init (case107).

References


Source files

  • CMakeLists.txt
  • app.cpp
  • v1.cpp
  • v1.h
  • v2.cpp
  • v2.h

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