Skip to content

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
Source files examples/case94_empty_tag_gained_state/

Category: Type Layout | Verdict: ๐Ÿ”ด BREAKING

What breaks

A type that was empty in v1 (sizeof == 1 per C++ rules) is no longer empty in v2. 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 callee reads partially uninitialized memory, then steps on subsequent stack/register state.

Real Failure Demo

Severity: BREAKING / LATENT CALL ABI DRIFT

The smoke app does not crash, but the public by-value tag changed from empty to stateful. Header-inlined algorithms compiled against v1 pass the old object shape into a v2 function expecting the wider tag.

cmake -S examples -B /tmp/abicheck-examples-build -DCMAKE_BUILD_TYPE=Debug
cmake --build /tmp/abicheck-examples-build --target case94_empty_tag_gained_state_app case94_empty_tag_gained_state_v2

tmp=$(mktemp -d)
cp /tmp/abicheck-examples-build/case94_empty_tag_gained_state/app_v1 "$tmp/"
cp /tmp/abicheck-examples-build/case94_empty_tag_gained_state/libv2.so "$tmp/libv1.so"
(cd "$tmp" && LD_LIBRARY_PATH=. ./app_v1)
# run(7) = 14 (expect 14)

Why this is a breaking change

The pattern mirrors the tbb::auto_partitioner / tbb::simple_partitioner / tbb::affinity_partitioner shape: empty tag types are passed by value into header-only algorithm wrappers (tbb::parallel_for, tbb::parallel_reduce). The library author sees the tag as an implementation detail with "no public members" โ€” but its sizeof is part of the ABI because consumers serialize the value at every call site.

This is exactly the failure mode affinity_partitioner had to engineer around: it does carry state, so it's intentionally non-copyable and only passed by reference.

Code diff

v1 v2
struct auto_partitioner {}; struct auto_partitioner { void* affinity_state_; };
sizeof == 1 (empty class rule) sizeof == 8 (pointer-sized)

How abicheck catches it

The existing TYPE_SIZE_CHANGED detector fires on the tag struct. STRUCT_FIELD_ADDED also fires (a previously-zero-field struct gained affinity_state_).

How to fix

If you need to add state to a previously-empty tag, the safe migration is: 1. Mark v1's tag as deprecated. 2. Introduce a new tag type (e.g. auto_partitioner_v2). 3. Provide a v1-compatible overload that ignores the old tag and converts. 4. Bump SONAME on the next ABI release.

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.

References


Source files

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

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