Skip to content

Case 23 โ€” Virtual Method Became Pure Virtual

Field Value
Verdict ๐Ÿ”ด BREAKING
Category Breaking
Platforms Linux, macOS, Windows
Flags ABI break, API break
Detected ChangeKinds func_pure_virtual_added
Source files browse on GitHub

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;
 };

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

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