Case 94: Empty Tag Gained State¶
| Field | Value |
|---|---|
| Verdict | ๐ด BREAKING |
| Category | Breaking |
| Platforms | Linux, macOS, Windows |
| Flags | ABI break, API break |
Detected ChangeKinds |
type_size_changed, type_field_added_compatible |
| Source files | examples/case94_empty_tag_gained_state/ |
Category: Type Layout | Verdict: ๐ด BREAKING
Verdict and consumer impact¶
A type that was empty in v1 (sizeof == 1, per the C++ empty-class rule)
is no longer empty in v2 (sizeof == 8). Any consumer compiled against v1
that passes the tag by value โ into a header-inline template, an algorithm
overload selector, or any function taking it by value โ wrote a 1-byte
argument into what is now an 8-byte parameter slot. The pattern mirrors
tbb::auto_partitioner/tbb::simple_partitioner: empty tag types passed
by value into header-only algorithm wrappers. The library author sees the
tag as an implementation detail with "no public members", but its sizeof
is part of the ABI because every call site serializes the value.
Old/new diff¶
| v1.h | v2.h |
|---|---|
struct auto_partitioner {}; (sizeof == 1) |
struct auto_partitioner { void* affinity_state_; }; (sizeof == 8) |
abicheck command¶
g++ -shared -fPIC -g -std=c++17 -I. v1.cpp -o libfoo_v1.so
g++ -shared -fPIC -g -std=c++17 -I. v2.cpp -o libfoo_v2.so
abicheck compare libfoo_v1.so libfoo_v2.so
Expected abicheck finding¶
Verdict: BREAKING (exit 4)
- type_size_changed: Size changed: mylib::auto_partitioner (8 -> 64 bits)
> Old code allocates or copies the type with the old size; heap/stack
corruption, out-of-bounds access.
Additions:
- type_field_added_compatible: Field added: mylib::auto_partitioner::affinity_state_
> Field appended without changing existing offsets; old code works but
won't initialize the new field.
Minimum evidence¶
min_evidence: L1 โ DWARF's struct-layout info (DW_TAG_structure_type
size) is enough to detect the empty-to-stateful size growth; no public
headers required. The field addition itself is correctly classified
type_field_added_compatible (non-polymorphic struct, no existing offsets
disturbed) โ a separate claim from the breaking size growth, which is
carried entirely by type_size_changed.
Why abicheck catches it¶
DWARF records each struct's total byte size for both versions; abicheck's
type_size_changed detector compares them directly. No layout inference or
header parsing is needed โ the size is an explicit DWARF attribute on the
DW_TAG_structure_type DIE.
Runtime failure demonstration¶
Severity: BREAKING (latent โ not observable in this minimal demo)
Scenario: app passes auto_partitioner{} by value into runner::run(),
compiled against v1's 1-byte tag; swap in v2's 8-byte tag without
recompiling the app.
# Build old library + app
g++ -shared -fPIC -g -std=c++17 -I. v1.cpp -o libfoo.so
g++ -g -std=c++17 -I. app.cpp -L. -lfoo -Wl,-rpath,. -o app
./app
# โ run(7) = 14 (expect 14)
# Swap in new library (no recompile)
g++ -shared -fPIC -g -std=c++17 -I. v2.cpp -o libfoo.so
./app
# โ run(7) = 14 (expect 14) โ unchanged; run() ignores its tag parameter
Why the demo doesn't crash: this minimal runner::run() never reads
its auto_partitioner parameter, so the size mismatch has no observable
effect here โ the risk is latent, not demonstrated by this toy consumer.
tbb::affinity_partitioner is the real-world case where a partitioner tag
does carry state read by the callee: passing a v1-sized argument into a
v2-sized parameter slot there corrupts the following stack/register
arguments the moment the callee actually reads past the old 1-byte extent.
Safe redesign¶
If you need to add state to a previously-empty tag: mark v1's tag
deprecated, introduce a new tag type (e.g. auto_partitioner_v2),
provide a v1-compatible overload that ignores the old tag and converts, and
bump the SONAME on the next ABI release rather than growing the existing
tag in place.
Real-world example: oneTBB's affinity_partitioner is intentionally
larger than the other partitioners and is the only one that's stateful โ
the library evolved this distinction specifically to avoid the
silent-corruption pattern this case demonstrates. See
oneTBB VERSIONING.md.
Cross-tool comparison¶
Not independently re-verified in this environment (abidiff unavailable
here) โ see case07's struct-layout case for a documented abidiff exit-code
comparison on the same class of change (a plain struct size growth).
Source files¶
CMakeLists.txtapp.cppv1.cppv1.hv2.cppv2.h
See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.