Skip to content

Case 169: Overload Added to a Previously Unique Function

Field Value
Verdict ๐ŸŸก COMPATIBLE_WITH_RISK
Category Risk
Platforms Linux, macOS, Windows
Flags โ€”
Detected ChangeKinds overload_added
Source files examples/case169_overload_added/

Category: Overload Resolution / Source Compatibility | Verdict: โš ๏ธ COMPATIBLE_WITH_RISK

Verdict and consumer impact

v2 adds a float overload next to the until-now unique units::to_celsius(double). Nothing breaks at the binary level: the double symbol (_ZN5units10to_celsiusEd) is byte-for-byte untouched and old binaries keep working. The risk fires at the next recompile of every consumer: a v1-era call like to_celsius(98.6f) promoted the float to double and bound to the only overload; recompiled against v2 headers, overload resolution now prefers the exact-match float overload โ€” a different result, different precision, no warning, no diff at the call site. Address-taking (&units::to_celsius) and template argument deduction also break outright, with a compile error rather than silent drift.

Old/new diff

v1.h v2.h
double to_celsius(double fahrenheit); double to_celsius(double fahrenheit); (unchanged)
(no other overload) float to_celsius(float fahrenheit); (new)

abicheck command

g++ -shared -fPIC -g v1.cpp -o libv1.so
g++ -shared -fPIC -g v2.cpp -o libv2.so
abicheck compare libv1.so libv2.so

Expected abicheck finding

Verdict: COMPATIBLE_WITH_RISK (exit 0)

Deployment Risk Changes:
- overload_added: Overload added to previously non-overloaded function:
  units::to_celsius โ€” &units::to_celsius becomes ambiguous and overload
  resolution may change

Additions:
- func_added: New public function: units::to_celsius(float)

Minimum evidence

min_evidence: L0 โ€” abicheck groups exported functions by their scope-qualified name (parsed structurally from the Itanium mangling of each .dynsym entry, e.g. _ZN5units10to_celsiusEd vs. _ZN5units10to_celsiusEf), so overload-set growth is visible from the symbol table alone; no debug info or headers needed.

Why abicheck catches it

abicheck reports overload_added (RISK) when a name that had exactly one declaration in the old snapshot gains siblings while the original symbol survives โ€” distinguishing it from a plain signature change (remove+add) and from an unrelated same-leaf name in a different scope. The relational signal (a name that was unique gained a sibling) is what a plain addition/removal diff can't express โ€” without overload-set awareness, this is indistinguishable from a harmless new function.

Runtime failure demonstration

Severity: INFORMATIONAL

Scenario: old binaries are untouched โ€” the demo shows binary compatibility holding, with the hazard waiting at recompile time.

# Build old library + app
g++ -shared -fPIC -g v1.cpp -o liblib.so
g++ -g app.cpp -L. -llib -Wl,-rpath,. -o app
./app
# โ†’ to_celsius(98.6f) = 37.0000 (expected 37.0)

# Swap in new library (no recompile)
g++ -shared -fPIC -g v2.cpp -o liblib.so
./app
# โ†’ to_celsius(98.6f) = 37.0000 (expected 37.0)   โ† binary-compatible

# The break is at recompile time, against the v2 header:
cat > amb.cpp <<'SRC'
#include "v2.h"
int main() { auto fp = &units::to_celsius; (void)fp; }
SRC
g++ -c amb.cpp -I.
# โ†’ error: unable to deduce 'auto' from '& units::to_celsius'

Why INFORMATIONAL, not CRITICAL: no existing binary crashes or misbehaves. The silent hazard is at the next recompile: rebuilding the original app source against v2 headers binds to_celsius(98.6f) to the new float overload โ€” the result is computed in float precision from then on, with no diagnostic anywhere.

Safe redesign

  1. Name the new function instead of overloading (to_celsius_f(), or a template with explicit constraints) when the existing name has shipped as unique.
  2. If the overload must land, document the re-routing in release notes and grep consumer code for address-taking (&to_celsius) and float-argument call sites.
  3. = delete the risky overload direction when the goal is to forbid lossy calls rather than add a fast path (float to_celsius(float) = delete; makes float callers explicit instead of silently re-routed โ€” that deletion is its own API break, but a loud one).

Real-world example: the KDE Frameworks binary-compatibility policy ("you can... add new non-virtual functions but note that adding an overload to a function that previously had none can break source compatibility") is the canonical write-up. std::filesystem::path construction and std::to_chars overload growth both triggered exactly this class of downstream deduction breakage during standard-library evolution.

References


Source files

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

See also: Examples overview ยท All COMPATIBLE_WITH_RISK cases ยท Category: Risk.