Case 21: Method Became Static¶
| Field | Value |
|---|---|
| Verdict | ๐ด BREAKING |
| Category | Breaking |
| Platforms | Linux |
| Flags | ABI break, API break |
Detected ChangeKinds |
func_static_changed |
| Source files | examples/case21_method_became_static/ |
Category: Breaking | Verdict: ๐ด BREAKING
Verdict and consumer impact¶
Per the Itanium C++ ABI, static-ness is not encoded in symbol mangling for
this case, so Widget::bar()'s symbol still resolves after the change
(_ZN6Widget3barEv in both versions) โ old binaries link fine against new.
But the calling contract changes underneath them: old's bar() is an
instance method that expects an implicit this pointer; new's bar() is
static and expects none. A caller still passing this gets whatever
garbage falls out of the mismatched calling convention โ no crash required,
just silently wrong results.
Old/new diff¶
| old/lib.h | new/lib.h |
|---|---|
class Widget { public: int value; int bar(); }; |
class Widget { public: static int bar(); }; |
abicheck command¶
g++ -shared -fPIC -std=c++17 -g old/lib.cpp -Iold -o libfoo_v1.so
g++ -shared -fPIC -std=c++17 -g new/lib.cpp -Inew -o libfoo_v2.so
abicheck compare libfoo_v1.so libfoo_v2.so
Expected abicheck finding¶
Verdict: BREAKING (exit 4)
- type_size_changed: Size changed: Widget (32 -> 8 bits)
> Old code allocates or copies the type with the old size; heap/stack
corruption, out-of-bounds access.
- type_field_removed: Field removed: Widget::value
> Old code accesses a field that no longer exists at the expected offset;
reads garbage or writes out of bounds.
Minimum evidence¶
min_evidence: L1 โ DWARF's struct-layout info already proves this is
BREAKING: dropping value to make bar() static also shrinks Widget
itself (32 โ 8 bits, the minimum size of an empty class), which
DW_TAG_structure_type size + member offsets capture directly. Note this
isn't quite the same fact as "method became static" โ DWARF's own
is_static concept (DW_AT_external) describes file-scope linkage, not
instance-vs-static class methods, so plain DWARF can't directly see the
qualifier change. Adding headers (L2, --ast-frontend clang here since this
sandbox has no castxml) additionally surfaces the more semantically precise
func_static_changed: Static qualifier changed: bar (False -> True) for the
same break โ see below.
Why abicheck catches it¶
At L1, abicheck compares each struct's DW_TAG_structure_type size and
member list directly from debug info; Widget losing its value field and
shrinking to an empty class's minimum size is unambiguous breaking evidence
on its own โ old code copying/allocating sizeof(Widget) now over-reads.
At L2, the header AST additionally carries each method's static
specifier, so abicheck can name the actual qualifier change:
abicheck compare libfoo_v1.so libfoo_v2.so \
--header old=old/lib.h --header new=new/lib.h --ast-frontend clang
- func_static_changed: Static qualifier changed: bar (False -> True)
> Static/non-static transition changes calling convention (implicit this
pointer); ABI mismatch.
Runtime failure demonstration¶
Severity: CRITICAL
Scenario: compile app against old (instance method, sets value=41,
expects bar() to return 42), swap in new .so without recompile.
# Build old library + app
g++ -shared -fPIC -std=c++17 -g old/lib.cpp -Iold -o libwidget.so
g++ -std=c++17 -g app.cpp -Iold -L. -lwidget -Wl,-rpath,. -o app
./app
# โ bar() called (instance method), value=41
# โ got=42 expected=42
# โ exit: 0
# Swap in new library (no recompile)
g++ -shared -fPIC -std=c++17 -g new/lib.cpp -Inew -o libwidget.so
./app
# โ bar() called (static method), returning fixed value
# โ got=7 expected=42
# โ WRONG RESULT: method call contract changed (instance -> static)
# โ exit: 1
Why CRITICAL: the symbol still links โ no loader error, no crash โ but
bar()'s calling contract silently changed. The app's compiled-in call
still passes this as if bar() were an instance method; the new static
bar() doesn't expect it, so the app observes a deterministically wrong
result rather than a diagnosable failure.
Safe redesign¶
Never turn a public instance method into a static one (or vice versa) in
place โ it changes the calling convention while keeping the same mangled
symbol, so linkers can't catch the mismatch. Add a new static function or
free function alongside the old instance method instead, and deprecate the
old one for at least one release cycle.
Real-world example: this pattern shows up when a class is refactored to
be "more functional" (e.g. removing per-instance state to make a method a
pure computation) without renaming the method โ exactly the Widget::bar()
shape here.
References¶
Source files¶
CMakeLists.txtapp.cpp
See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.