Skip to content

Case 23 — Virtual Method Became Pure Virtual

Field Value
Verdict 🔴 BREAKING
Category Breaking
Classification Rule
Platforms Linux, macOS, Windows
Flags ABI break, API break
Detected ChangeKinds func_virtual_became_pure
Source files catalog/cases/case23_pure_virtual_added/
Rule family pure-virtual-added
Subject Vtable and virtual dispatch changes

Verdict: 🔴 BREAKING abicheck verdict: BREAKING

What changes

Version Declaration
v1 class Processor { virtual void process(); };
v2 class Processor { virtual void process() = 0; };

What breaks at binary level

Making Processor::process() pure virtual (= 0) has two ABI consequences:

  1. The vtable entry for process() is replaced — the slot that previously pointed to Processor's concrete implementation of process() now points to the pure-call handler (__cxa_pure_virtual). Already-compiled consumers that invoke process() through a Processor* vtable dispatch will hit the pure-call handler at runtime, causing std::terminate instead of calling the old base implementation.

  2. Processor becomes abstract — source-level rebuilds will fail to compile new Processor() (abstract class cannot be instantiated). For already-compiled binaries this is not the direct failure mode; the runtime break comes from point 1 above (dispatch to the pure-call handler via the vtable slot).

Consumer impact

/* consumer compiled against v1 (concrete class) */
Processor* p = new Processor();
p->process();  /* calls concrete implementation */

/* with v2: Processor is abstract */
/* vtable slot points to __cxa_pure_virtual */
/* → runtime abort: "pure virtual method called" */

For plugin architectures where downstream code extends the interface:

/* old plugin implements only process() */
struct MyPlugin : Processor {
    void process() override;
};
/* this still works — but any new pure virtual methods
   added to Processor would break existing plugins */

Mitigation

  • Create Processor2 (or IProcessor) as the new abstract interface.
  • Keep the original Processor class frozen for existing consumers.
  • Version plugin interfaces explicitly.

Code diff

 class Processor {
 public:
-    virtual void process();
+    virtual void process() = 0;
 };

new/lib.cpp also keeps an out-of-line void Processor::process() { ... } body alongside the pure declarator. A pure virtual function may still have a definition — it's reachable only via an explicit qualified call (Processor::process()), never through the vtable of the abstract class itself, so this changes no runtime behavior. It exists so v2's binary/DWARF still carries a Processor::process() symbol to compare against v1's, letting abicheck's virtual-to-pure-virtual detector see this as one function changing state (FUNC_VIRTUAL_BECAME_PURE) instead of a plain removal.

abicheck's diff_types.py distinguishes two related kinds sharing the same detection code: FUNC_VIRTUAL_BECAME_PURE (this case — an already-virtual concrete method becomes pure) vs. FUNC_PURE_VIRTUAL_ADDED (a non-virtual function becomes pure virtual, a rarer shape). ground_truth.json's expected_kinds was previously the latter, which is not what this fixture demonstrates.

Real Failure Demo

Severity: CRITICAL

Scenario: app calls process() via vtable. With v2 the vtable slot points to __cxa_pure_virtual → abort().

# Build old lib + app
g++ -shared -fPIC -g old/lib.cpp -Iold -o libproc.so
g++ -g app.cpp -Iold -L. -lproc -Wl,-rpath,. -o app
./app
# → Calling process()...
# → processing
# → Done.

# Swap in new lib (pure virtual → abort)
g++ -shared -fPIC -g new/lib.cpp -Inew -o libproc.so
./app
# → Calling process()...
# → pure virtual method called
# → Aborted (core dumped)

Why CRITICAL: Existing binaries that instantiate Processor and call process() via the vtable now hit the pure-virtual handler, causing unconditional abort(). Every plugin or subclass compiled against v1 must be rebuilt with the new abstract interface.

Why runtime result may differ from verdict

Became pure virtual: direct instantiation causes SIGABRT

References


Source files

  • new/
  • old/
  • CMakeLists.txt
  • app.cpp

See also: Compatibility Catalog · All BREAKING cases · Category: Breaking · Rule: Virtual function became pure · Subject: Vtable and virtual dispatch changes.