Skip to content

Case 87: Default Template Argument Changed

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

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

Verdict and consumer impact

// v1
template <typename Float, typename Distance = minkowski_distance<Float>>
class descriptor;

// v2
template <typename Float, typename Distance = euclidean_distance<Float>>
class descriptor;

Consumer source like mylib::descriptor<float> d; compiles unchanged against both headers โ€” the default-argument change is invisible at the call site. But the substituted type differs, so the mangled symbol differs too: v1 ships descriptor<float, minkowski_distance<float>>, v2 ships descriptor<float, euclidean_distance<float>>. A consumer compiled against v1's header calls the first mangled name; the v2 library exports only the second. Recompilation is mandatory even though nothing in the consumer's own source changed.

Old/new diff

v1.h v2.h
template <typename Float, typename Distance = minkowski_distance<Float>> class descriptor; template <typename Float, typename Distance = euclidean_distance<Float>> class descriptor;

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)

- default_template_arg_changed: Template instantiation 'descriptor'
  substitutes to different arguments than its surviving sibling
  'descriptor'. Consistent with a change to a default template argument
  in the declaring header: consumer source compiles unchanged, but the
  substituted mangled symbol differs.
  (float, mylib::minkowski_distance<float> -> float, mylib::euclidean_distance<float>)
  > Consumers built against the old default reference a symbol that no
    longer exists. Unlike function default parameter changes (NO_CHANGE),
    template default arguments ARE part of the substituted type and
    affect mangling.

- instantiation_missing_from_binary / func_removed (per instantiation):
  the old library's descriptor<float, minkowski_distance<float>> symbols
  are gone; only descriptor<float, euclidean_distance<float>> ships in v2.

Minimum evidence

min_evidence: L2 โ€” the raw symbol diff shows an unrelated-looking add/remove pair (old mangled name gone, new one added); recovering the template's default argument fact needs the public header AST, since default_template_arg_changed's detector demangles Function.mangled to recover the template-argument-embedded name and correlates it against the default declared in the header. 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

detect_default_template_arg_changed (abicheck/diff_cpp_patterns.py) demangles each removed/added function's mangled name to recover its substituted template arguments, groups instantiations by their unqualified template name, and flags a surviving sibling whose only difference is the substituted-default-argument slot โ€” the header AST confirms which parameter actually carries a default, distinguishing this from an unrelated overload change.

Runtime failure demonstration

Severity: CRITICAL

Scenario: app instantiates descriptor<float> using v1's default (minkowski_distance<float>); v2's library only ships the euclidean_distance<float> instantiation.

# 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
# โ†’ dim = 0

# 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:
#   _ZNK5mylib10descriptorIfNS_18minkowski_distanceIfEEE9dimensionEv

Why CRITICAL: the app's own source (mylib::descriptor<float> d;) never changed and recompiles cleanly against either header โ€” the failure only shows up as a runtime unresolved-symbol error against a library built from the other header's default.

Safe redesign

Never change a template's default argument once instantiations of it have shipped โ€” it's part of the substituted type, not a source-only convenience. Either keep the old default and add an explicitly-named alias for the new behavior (descriptor_euclidean<Float>), or bump the SONAME/major version so old and new defaults can never mix in the same deployment.

Real-world example: oneDAL's cpp/oneapi/dal/algo/knn/common.hpp declares descriptor<Float, Method, Task, Distance = ...> with several defaulted template parameters; changing any one of them re-mangles every explicit instantiation that didn't override that argument.

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 case02's parameter-type-change 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.