Skip to content

Case 22: Method Const Qualifier Changed

Field Value
Verdict 🔴 BREAKING
Category Breaking
Classification Rule
Platforms Linux, macOS, Windows
Flags ABI break, API break
Detected ChangeKinds func_cv_changed
Source files catalog/cases/case22_method_const_changed/
Rule family method-const-changed
Subject Function-signature and source-API changes

Category: Breaking | Verdict: 🔴 BREAKING

Verdict and consumer impact

In the Itanium C++ ABI, const qualification on a member function is encoded directly into the mangled symbol name: Widget::get() const mangles to _ZNK6Widget3getEv (the K marks const); dropping const mangles to _ZN6Widget3getEv — a completely different symbol. Old binaries import _ZNK6Widget3getEv; new's library exports only _ZN6Widget3getEv. The dynamic linker cannot resolve the reference — an unresolved-symbol error at load or first-call time, not a subtle miscalculation.

Old/new diff

old/lib.h new/lib.h
class Widget { public: void get() const; }; class Widget { public: void get(); };

abicheck command

g++ -shared -fPIC -std=c++17 -g old/lib.cpp -Iold -o libfoo_v1.so
g++ -shared -fPIC -std=c++17 -g new/lib.cpp -Inew -o libfoo_v2.so
abicheck compare libfoo_v1.so libfoo_v2.so

Expected abicheck finding

Verdict: BREAKING (exit 4)

- func_removed_elf_only: Elf_only function removed: Widget::get() const
  > Exported function symbol removed from the binary; old binaries that
    link or dlsym() it can fail even without header evidence.

Additions:
- func_added: New public function: Widget::get()
  > New function available; existing binaries are unaffected.

Minimum evidence

min_evidence: L1 — because const is baked into the Itanium mangled name itself, the exported-symbol table already shows two unrelated symbols (_ZNK6Widget3getEv gone, _ZN6Widget3getEv new) — this is detectable from ELF/DWARF symbol evidence alone, no public headers required. Headers (L2) sharpen the finding from a generic removed+added pair into a named CV-qualifier change — see below — but aren't needed to reach the BREAKING verdict.

Why abicheck catches it

At L1, abicheck's exported-symbol diff sees _ZNK6Widget3getEv present only in old and _ZN6Widget3getEv present only in new, with no other correlating evidence to link them, so it reports a straightforward func_removed_elf_only + func_added pair — already BREAKING, since a symbol old binaries depend on is gone. With header AST evidence (L2), the same two functions are recognized as the same declaration source (Widget::get, old/lib.h:3) with a changed const qualifier, so the finding gets renamed to the more specific func_cv_changed:

cat > .abicheck.yml <<'EOF'
compile:
  frontend: clang
EOF
abicheck compare libfoo_v1.so libfoo_v2.so --header old=old/lib.h --header new=new/lib.h --config .abicheck.yml
- func_cv_changed: CV qualifier changed: get (const=True volatile=False ->
  const=False volatile=False)
  > const/volatile on 'this' changes the mangled name; old binaries link to
    the wrong symbol.

Runtime failure demonstration

Severity: CRITICAL

Scenario: app compiled against old header calls get() const (_ZNK6Widget3getEv). new's library exports only _ZN6Widget3getEv — undefined symbol at runtime.

# Build old library + app
g++ -shared -fPIC -std=c++17 -g old/lib.cpp -Iold -o libwidget.so
g++ -std=c++17 -g app.cpp -Iold -L. -lwidget -Wl,-rpath,. -o app
./app
# → get() const called
# → exit: 0

# Swap in new library (const removed)
g++ -shared -fPIC -std=c++17 -g new/lib.cpp -Inew -o libwidget.so
./app
# → ./app: symbol lookup error: ./app: undefined symbol: _ZNK6Widget3getEv
# → exit: 127

Why CRITICAL: const is part of the C++ mangled name (K in _ZNK...). Removing it produces a completely different symbol. Every pre-built binary that calls widget.get() on a const reference fails to load — a hard runtime crash, not a silent miscalculation.

Safe redesign

Keep the old const-qualified overload and add the new non-const one alongside it if the semantics genuinely need to differ; if the const qualifier was simply wrong, ship both overloads during a transition period so binaries built against either one keep resolving.

Real-world example: const-correctness cleanups that touch an already-shipped public method signature are a classic source of this — every mangled-name change from a const fix is, by ABI rules, indistinguishable from removing one function and adding an unrelated one.

References


Source files

  • new/
  • old/
  • CMakeLists.txt
  • app.cpp

See also: Compatibility Catalog · All BREAKING cases · Category: Breaking · Rule: Member function cv-qualification changed · Subject: Function-signature and source-API changes.