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¶
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.txtapp.cppv1.cppv1.hv2.cppv2.h
See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.