Skip to content

Case 79: Missing Template Instantiation in Shipped Binary

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

Category: Header-vs-Binary Parity | Verdict: ๐Ÿ”ด BREAKING

Verdict and consumer impact

v1 ships two explicit template instantiations, descriptor<float> and descriptor<double>, and the header advertises both via extern template. v2's header is unchanged โ€” it still declares extern template class descriptor<double> โ€” but the .cpp silently drops the template class descriptor<double>; explicit-instantiation line. Consumer source compiles cleanly against v2's header (no diagnostic at all), but linking/loading against libv2.so fails with an undefined symbol. This is the failure mode behind oneDAL's "ship one Float combination only" build trims when a header isn't updated to match.

Old/new diff

v1.cpp v2.cpp
template class descriptor<float>; template class descriptor<float>;
template class descriptor<double>; (removed โ€” but v1.h/v2.h both still declare extern template class descriptor<double>)

abicheck command

g++ -std=c++17 -shared -fPIC -g v1.cpp -o libfoo_v1.so
g++ -std=c++17 -shared -fPIC -g v2.cpp -o libfoo_v2.so
abicheck compare libfoo_v1.so libfoo_v2.so -H old=v1.h -H new=v2.h --ast-frontend clang

Expected abicheck finding

Verdict: BREAKING (exit 4)

- instantiation_missing_from_binary: Template instantiation 'descriptor'
  was exported by the old library but is missing from the new binary.
  Other instantiations of 'mylib::descriptor::descriptor()' still exist,
  so the public header very likely still advertises this one. Consumers
  built against the old header link cleanly but fail at load time with an
  undefined-symbol error. (mylib::descriptor<double>::descriptor())
- instantiation_missing_from_binary: ... mylib::descriptor<double>::threshold() const
- func_removed_elf_only: Elf_only function removed:
  mylib::descriptor<double>::set_threshold(double)
- func_removed_elf_only: Elf_only function removed:
  mylib::descriptor<double>::descriptor()
- func_removed_elf_only: Elf_only function removed:
  mylib::descriptor<double>::threshold() const

Minimum evidence

min_evidence: L2 โ€” the public header AST is what tells abicheck the header still declares extern template class descriptor<double> even though the binary no longer exports its mangled symbols; instantiation_missing_from_binary demangles each Function.mangled name to recover the template argument (<double>) that the header/castxml dumper's bare Function.name never carries, and scopes the check to Visibility.PUBLIC functions so a stale extern template cross-reference entry isn't mistaken for a surviving instantiation. castxml is the documented default backend for this evidence layer; clang (--ast-frontend clang) is a supported alternative AST frontend used above.

Why abicheck catches it

Without header evidence, a plain symbol diff would just report func_removed_elf_only for each of descriptor<double>'s three mangled symbols โ€” already enough to conclude BREAKING. With the header AST, abicheck additionally recognizes that the declaring header is unchanged and still promises descriptor<double>, which is the specific signal that distinguishes "the API author removed this on purpose" (an ordinary func_removed, source-visible via a compile error) from "the API author thinks this still ships but it doesn't" (instantiation_missing_from_binary โ€” no source-level diagnostic fires at all, only a load-time failure).

Runtime failure demonstration

Severity: CRITICAL

Scenario: compile app against v1, swap in v2 .so without recompile โ€” the app only uses the header's extern template declaration, so nothing about its own compilation changes.

# Build old library + app
g++ -std=c++17 -shared -fPIC -g v1.cpp -o libfoo.so
g++ -std=c++17 -g app.cpp -L. -lfoo -Wl,-rpath,. -o app
./app
# โ†’ threshold = 0.500000

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

Why CRITICAL: the app's own source never changed and gave no warning at compile time โ€” descriptor<double> is still declared extern template in the header the app compiled against. The failure surfaces only at dynamic-link time, in production, with no corresponding source-level signal a developer could have caught earlier.

Safe redesign

Keep the shipped *_instantiation.cpp file's explicit-instantiation list in lockstep with the header's extern template declarations โ€” ideally generated from the same combinatorics list, so a build trim that drops an instantiation is forced to also touch the header (turning a silent load-time failure into a compile-time one).

Real-world example: oneDAL's algorithm modules each have an *_instantiation.cpp file (one per Float ร— Method ร— Task combination). If a build is trimmed without updating headers, the binary stops shipping symbols the header still advertises โ€” exactly the scenario this case reproduces with a minimal two-instantiation example.


Source files

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

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