Skip to content

Case 166: Method Ref-Qualifier Added (str() → str() &)

Field Value
Verdict 🔴 BREAKING
Category Breaking
Classification Rule
Platforms Linux, macOS
Flags ABI break, API break
Detected ChangeKinds func_ref_qual_changed
Source files catalog/cases/case166_ref_qualifier_added/
Rule family ref-qualifier-added
Subject Function-signature and source-API changes

Category: Function Signature / Mangling | Verdict: 🔴 BREAKING

Verdict and consumer impact

v2 adds an lvalue ref-qualifier to MessageBuilder::str(). The motivation is sound API hardening — MessageBuilder().str() returns a pointer into a temporary that dies at the end of the expression, and & makes that dangling call a compile error — but the ref-qualifier is part of the Itanium mangling, encoded right after the CV-qualifiers in the nested-name. The old symbol _ZN14MessageBuilder3strEv vanishes from the library entirely (the new one is _ZNR14MessageBuilder3strEv), so every existing binary fails at load time with an undefined-symbol error — recompilation is mandatory even though no call site's source changed.

Old/new diff

v1.h v2.h
const char* str(); (_ZN14MessageBuilder3strEv) const char* str() &; (_ZNR14MessageBuilder3strEv)

abicheck command

g++ -shared -fPIC -g v1.cpp -o libv1.so
g++ -shared -fPIC -g v2.cpp -o libv2.so
cat > .abicheck.yml <<'EOF'
compile:
  frontend: clang
EOF
abicheck compare libv1.so libv2.so --header old=v1.h --header new=v2.h --config .abicheck.yml

Expected abicheck finding

Verdict: BREAKING (exit 4)

- func_removed: Public function removed: str
  > Old binaries call a symbol that no longer exists; dynamic linker
    will refuse to load or crash at call site.
- func_ref_qual_changed: Ref-qualifier changed: str ('' → '&')
  > Ref-qualifier (&/&&) on a member function changed; this alters the
    Itanium C++ ABI mangled name and overload resolution, so old
    binaries link to the wrong symbol or fail to resolve it.

Additions:
- func_added: New public function: str

abicheck matches the removed and added declarations by (name, parameters) and reports the pair as func_ref_qual_changed instead of leaving an unexplained removed+added pair unlinked.

Minimum evidence

min_evidence: L2 — released castxml versions do not emit a ref-qualifier attribute on the AST node, so abicheck needs the public header AST plus the Itanium mangling (_ZNR…/_ZNO…) to recover the qualifier; DWARF alone does not carry it. castxml is the documented default backend for this evidence layer; clang (compile.frontend: clang (via .abicheck.yml), used above) is a supported alternative AST frontend for hosts without castxml installed.

Why abicheck catches it

abicheck parses the public header AST for each side's str() declaration and cross-references the mangled symbol name in .dynsym; the ref-qualifier bit recovered from the mangling (R = &, O = &&, absent = unqualified) lets it link the "removed" old symbol and "added" new symbol as one func_ref_qual_changed event rather than an opaque removal/addition pair.

Runtime failure demonstration

Severity: CRITICAL

Scenario: compile app against v1, swap in v2 .so without recompile.

# Build old library + app
g++ -shared -fPIC -g v1.cpp -o liblib.so
g++ -g app.cpp -L. -llib -Wl,-rpath,. -o app
./app
# → message = status=ok (expected status=ok)

# Swap in new library (no recompile)
g++ -shared -fPIC -g v2.cpp -o liblib.so
./app
# → ./app: symbol lookup error: ./app: undefined symbol: _ZN14MessageBuilder3strEv

Why CRITICAL: the app aborts before main() finishes symbol binding — the classic undefined-symbol loader failure — even though no call site in the app's source changed.

Safe redesign

  1. Add, don't replace: keep the unqualified str() exported (possibly as a deprecated out-of-line definition forwarding to the qualified one) until the next SONAME bump.
  2. Qualify at birth: decide &/&& when the method is first shipped; qualifiers on day one cost nothing.
  3. SONAME bump if the hardening must land now.

Real-world example: this is the same mechanics as a const-qualifier break, which the KDE binary-compatibility policy lists among the "you cannot..." rules: any change to a function's cv- or ref-qualification changes the mangled name. The &&-qualified accessor idiom popularized by std::optional::value() && (C++17) made retrofit-qualifying older accessors a recurring temptation in library changelogs.

References


Source files

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

See also: Compatibility Catalog · All BREAKING cases · Category: Breaking · Rule: Reference qualifier added · Subject: Function-signature and source-API changes.