Case 85: Internal Template Signature Changed¶
| Field | Value |
|---|---|
| Verdict | ๐ด BREAKING |
| Category | Breaking |
| Platforms | Linux, macOS |
| Flags | ABI break |
Detected ChangeKinds |
internal_template_leaks_via_public_api, func_removed |
| Source files | examples/case85_internal_template_signature_changed/ |
Category: Template ABI | Verdict: ๐ด BREAKING
Verdict and consumer impact¶
lib::__detail::walk<T> looks private (it lives in a __detail namespace),
but the public inline algorithm lib::sum_range<T> dispatches through it โ
so every consumer that ever instantiated sum_range<float> has
__detail::walk<float> baked into its own symbol table via extern
template. In v2 the library's explicit-instantiation set shifts
(walk<float> dropped, walk<double> added). A binary built against v1
that calls sum_range<float>(...) fails to resolve walk<float> the moment
v2's library is loaded โ recompilation is mandatory even though the caller
never named __detail::walk directly.
Old/new diff¶
| v1.h | v2.h |
|---|---|
extern template int walk<int>(int*, int*); |
extern template int walk<int>(int*, int*); |
extern template float walk<float>(float*, float*); |
extern template double walk<double>(double*, double*); |
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 \
--ast-frontend clang -H old=v1.h -H new=v2.h --lang c++
Expected abicheck finding¶
Verdict: BREAKING (exit 4)
- func_removed: Public function removed: walk
> Old binaries call a symbol that no longer exists; dynamic linker
will refuse to load or crash at call site.
- internal_template_leaks_via_public_api: Internal-namespace function
template 'float lib::__detail::walk(float*, float*)' has changed
instantiations: removed=['float lib::__detail::walk<float>(float*, float*)'], added=[]
> An internal-namespace function template changed signature, and its
instantiations appear in consumer symbol tables because public
algorithms inline-dispatch through it. Every consumer must rebuild.
Additions:
- func_added: New public function: walk (double lib::__detail::walk<double>(double*, double*))
Minimum evidence¶
min_evidence: L2 โ the exported symbols alone show a plain add/remove
pair (walk<float> gone, walk<double> new); distinguishing "an internal
template's instantiation set shifted, and it leaks through a public inline
algorithm" from an unrelated add+remove needs the header AST to see that
__detail::walk is dispatched to from sum_range's inline body. castxml
is the documented default header backend; a clang-based AST frontend is a
supported alternative that reaches the same evidence.
Why abicheck catches it¶
The header AST resolves sum_range's inline call graph and confirms it
reaches __detail::walk<T>, so a change to __detail's explicit
instantiation set is correlated with the public entry point that pulls it
into every consumer's symbol table โ the function-template analogue of
internal_type_leaks_via_public_api.
Runtime failure demonstration¶
Severity: CRITICAL
Scenario: app instantiates sum_range<float>, which pulls
__detail::walk<float> into its own undefined-symbol table via extern
template. Swap in v2's library without recompiling.
# 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
# โ exit 0 (sum_range<int> and sum_range<float> both resolve)
# 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: _ZN3lib8__detail4walkIfEET_PS2_S3_
Why CRITICAL: the app never spelled __detail::walk in its own source โ
the symbol was pulled in transitively via sum_range<float>'s inline body โ
so the failure is invisible from reading the consumer's code, only from its
compiled symbol table.
Safe redesign¶
Never let a public inline/template API dispatch to an internal helper via
extern template with a instantiation set that can shift between releases.
Either instantiate every type the public API supports and freeze that set,
or move the helper out-of-line behind a stable, non-template public entry
point so its symbol doesn't depend on caller-side instantiation.
Real-world example: the function-template counterpart of PR #238's
internal type leak detection โ the same failure mode libstdc++/libc++
avoid by keeping internal helper templates either fully static/anonymous
or free of caller-visible instantiation churn across releases.
Cross-tool comparison¶
Not independently re-verified in this environment (abidiff unavailable
here) โ see case02's parameter-type-change 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.