Skip to content

Case 86: Tag Struct Renamed (empty class re-mangling)

Field Value
Verdict ๐Ÿ”ด BREAKING
Category Breaking
Platforms Linux, macOS, Windows
Flags ABI break, API break
Detected ChangeKinds tag_type_renamed
Source files examples/case86_tag_struct_renamed/

Category: Mangling ABI | Verdict: ๐Ÿ”ด BREAKING

Verdict and consumer impact

Empty tag structs are the workhorses of C++ template specialization (mylib::method::brute_force, mylib::method::kd_tree, ...). They carry no layout, but every explicit instantiation that names them bakes the tag into its mangled symbol. v2 renames brute_force โ†’ search_brute: the type itself has zero fields so layout-based detectors see nothing, but every descriptor<brute_force, ...> instantiation symbol re-mangles and the old symbols vanish. A consumer binary linked against v1 fails to resolve those symbols at load time โ€” recompilation is mandatory even though no field or method actually changed.

Old/new diff

v1.h v2.h
namespace method { struct brute_force {}; struct kd_tree {}; } namespace method { struct search_brute {}; struct kd_tree {}; }
extern template class descriptor<method::brute_force, task::classification>; extern template class descriptor<method::search_brute, task::classification>;

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)

- tag_type_renamed: Empty tag struct 'mylib::method::brute_force' renamed
  to 'mylib::method::search_brute'. The type has no fields or vtable, so
  layout-based detectors see no change, but 3 explicit instantiation
  symbol(s) referencing the old name were re-mangled (now 3 symbol(s)
  reference the new name). Consumers built against the old header fail to
  resolve the instantiation at load time.
  > An empty tag struct used solely for template specialization was
    renamed. Every explicit instantiation that referenced the old tag is
    re-mangled and the old symbol disappears.
  Affected symbols: mylib::descriptor<mylib::method::brute_force,
  mylib::task::classification>::descriptor(), ...::kind() const

Minimum evidence

min_evidence: L0 โ€” the exported-symbol table alone is enough to reach the BREAKING verdict: even on a fully stripped binary, abicheck's binary fingerprinting matches each renamed instantiation by identical code size/hash and reports func_likely_renamed. The specific tag_type_renamed diagnosis shown above additionally needs DWARF's type_removed/type_added pair for the empty struct โ€” present once the binaries are built with -g (L1) โ€” to correlate the rename with the tag type itself rather than leaving it as an unexplained heuristic symbol match.

Why abicheck catches it

detect_tag_type_renamed (abicheck/diff_cpp_patterns.py) looks for an empty-record TYPE_REMOVED/TYPE_ADDED pair in the same parent namespace, then confirms a symbol-rename pair exists whose old/new mangled names embed the removed/added tag's name segment โ€” correlating the "invisible" tag rename with the very real re-mangling of every instantiation that used it.

Runtime failure demonstration

Severity: CRITICAL

Scenario: compile app against v1, swap in v2 .so without recompile.

# 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
# โ†’ kind = 1

# Swap in new library (no recompile)
g++ -shared -fPIC -g -std=c++17 -I. v2.cpp -o libfoo.so
./app
# โ†’ ./app: symbol lookup error: ./app: undefined symbol:
#   _ZN5mylib10descriptorINS_6method11brute_forceENS_4task14classificationEEC1Ev

Why CRITICAL: the descriptor<method::brute_force, task::classification> constructor's mangled name embeds brute_force's name segment; once the tag is renamed, that exact symbol no longer exists in the new library and the dynamic linker refuses to load the old binary.

Safe redesign

Never rename an empty tag struct used in shipped explicit instantiations โ€” even though "nothing about the type changed", every instantiation that named it re-mangles. Add a new tag alongside the old one and deprecate, or keep the old tag as a type alias (using brute_force = search_brute;) so its mangled identity survives even though the "canonical" name moves.

Real-world example: oneDAL's cpp/oneapi/dal/algo/*/common.hpp defines method::*/task::* tag families exactly like this; every descriptor<Float, Method, Task> instantiation embeds them in its mangled name, so renaming any tag silently breaks every shipped instantiation symbol.

Cross-tool comparison

abidw --out-file v1.xml libfoo_v1.so
abidw --out-file v2.xml libfoo_v2.so
abidiff v1.xml v2.xml

Not independently re-verified in this environment (abidiff unavailable here) โ€” see case01's symbol-removal case for a documented abidiff exit-code comparison.


Source files

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

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