Skip to content

Case 125: Class Became final

Field Value
Verdict ๐ŸŸ  API_BREAK
Category API Break
Platforms Linux, macOS, Windows
Flags API break
Detected ChangeKinds type_became_final
Source files examples/case125_class_became_final/

Category: API Break | Verdict: ๐ŸŸ  API_BREAK

Verdict and consumer impact

Shape gains the final specifier in v2. Its size, alignment, vtable, and every member's mangled name are unchanged, so already-compiled binaries keep linking and running against either version โ€” this is not a runtime ABI break. But any consumer that derives from Shape (struct MyShape : public Shape { ... }) no longer compiles: recompiling an existing derived-class consumer against the v2 header fails outright.

Old/new diff

v1.h v2.h
class Shape { ... }; class Shape final { ... };

abicheck command

g++ -shared -fPIC -g v1.cpp -o libshape_v1.so
g++ -shared -fPIC -g v2.cpp -o libshape_v2.so
ABICHECK_AST_FRONTEND=clang abicheck compare libshape_v1.so libshape_v2.so \
    --header old=v1.h --header new=v2.h

Expected abicheck finding

Verdict: API_BREAK (exit 2)

- type_became_final: Class gained `final` specifier: Shape --
  consumers that derive from it no longer compile (non-final -> final)
  > A class/struct gained the `final` specifier. Any consumer that
    derives from it no longer compiles. Type layout and mangled names
    are unchanged so already-built binaries keep running, but
    recompilation against the new header fails.

Minimum evidence

min_evidence: L2 โ€” final is a pure C++ source-level specifier. It is not recorded in DWARF or anywhere in the object file (abicheck compare with no headers reports NO_CHANGE on this pair), so only the public header's AST can see it. castxml is abicheck's default L2 backend; the run above uses the clang AST frontend (ABICHECK_AST_FRONTEND=clang) as a supported alternative โ€” either backend records the class-key.

Why abicheck catches it

The header-AST parser records each class/struct's final class-key from the declaration itself; abicheck compares that flag between the two header snapshots and reports type_became_final when it flips from absent to present, scoped to declarations in the headers actually passed on the command line.

Runtime failure demonstration

Severity: not a runtime crash โ€” a compile-time break for derived-class consumers

Because final changes no layout or symbol, swapping the library binary alone is silent; the break only shows up when a derived-class consumer is recompiled against the new header.

# app.cpp derives from Shape: `struct MyShape : public Shape { ... };`

# Compile + link against v1 (non-final) -- succeeds and runs:
g++ -shared -fPIC -g v1.cpp -o libshape.so
g++ -std=c++17 -g app.cpp -I. -L. -lshape -Wl,-rpath,. -o app
./app
# -> area=1.000000 extra=0

# Recompile the SAME app.cpp against v2.h (final) -- fails to compile:
g++ -std=c++17 -g -DUSE_V2 app.cpp -I. -L. -lshape -Wl,-rpath,. -o app_v2
# -> error: cannot derive from 'final' base 'Shape' in derived type 'MyShape'

Why this matters anyway: the compiled .so files stay ABI-identical (swapping the library binary produces no crash), so this break is invisible to any binary/DWARF-only inspection. It only surfaces the moment a derived-class consumer's source is rebuilt against the new header โ€” exactly the scenario abicheck's header mode is built to catch ahead of time.

Safe redesign

Keep public base classes non-final, or document the inheritance contract explicitly. Adding final to a previously-extensible public class is a breaking API change and warrants a major version bump.

Cross-tool comparison

abidiff/abidw operate on the compiled ELF and DWARF debug info only โ€” final leaves no trace there (the v1 and v2 .so files are ABI-identical), so a binary-only comparison tool reports no change at all here, the same way abicheck's own object-only mode does. Only a header-aware comparison (abicheck's L2 mode) sees this class of change.


Source files

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

See also: Examples overview ยท All API_BREAK cases ยท Category: API Break.