Skip to content

Case 96: Hidden Friend Operator Removed

Field Value
Verdict ๐ŸŸ  API_BREAK
Category API Break
Platforms Linux, macOS, Windows
Flags API break
Detected ChangeKinds hidden_friend_removed
Source files examples/case96_hidden_friend_removed/

Category: Source API contract | Verdict: ๐ŸŸ  API_BREAK

Verdict and consumer impact

A non-member operator== was declared as a hidden friend โ€” an in-class friend declaration with an inline definition, findable only via argument-dependent lookup (ADL). v2 removes the declaration. The library's .so is byte-identical (the inline friend never had an external symbol), so nothing breaks at link time โ€” but every consumer that wrote a == b against v1's header fails to compile against v2's, because ADL no longer finds a viable operator== on mylib::point. Recompilation is not merely required, it's impossible without a source change at the call site.

Old/new diff

v1.h v2.h
friend bool operator==(const point& a, const point& b) { ... } inside point (removed โ€” no operator== at all)

abicheck command

g++ -std=c++17 -shared -fPIC -g v1.cpp -o libmylib_v1.so
g++ -std=c++17 -shared -fPIC -g v2.cpp -o libmylib_v2.so
abicheck compare libmylib_v1.so libmylib_v2.so \
  --header old=v1.h --header new=v2.h --ast-frontend clang

Expected abicheck finding

Verdict: API_BREAK (exit 2)

## Source-Level Breaks

- hidden_friend_removed: Hidden friend declaration removed: operator==
  > An in-class `friend` declaration (a 'hidden friend' โ€” findable only
    via ADL on one of its argument types) was removed. Inline hidden
    friends never receive an external symbol, so the break is invisible
    at the binary layer, but every consumer that wrote `a == b` fails to
    compile against the new headers.

Minimum evidence

min_evidence: L2 โ€” the removed declaration never had an external symbol (it's an inline hidden friend), so neither the symbol table (L0) nor DWARF (L1) shows any difference between v1 and v2. Only the public-header AST records that point no longer befriends an operator==; castxml is the documented default AST backend for this (--ast-frontend clang is a supported alternative, used here since castxml isn't installed in this environment).

Why abicheck catches it

The header-AST dumper reads castxml's (or, here, clang's) befriending attribute on the Class/Struct element โ€” a list of function elements declared as in-class friend. Each is marked is_hidden_friend=True in the snapshot; the diff reports hidden_friend_removed for any hidden friend present in v1's header AST and absent from v2's.

Runtime failure demonstration

Severity: API BREAK (source-only โ€” the binaries themselves keep linking)

# Compile the consumer against v1's header โ€” succeeds and runs.
g++ -std=c++17 -I. app.cpp -L. -lmylib_v1 -Wl,-rpath,. -o app_v1
./app_v1
# โ†’ a == b โ†’ true (expect true)

# Swap in v2's header (the same app.cpp source, no other change) โ€” fails to compile:
g++ -std=c++17 -I. app.cpp -L. -lmylib_v2 -Wl,-rpath,. -o app_v2
# โ†’ app.cpp:19:18: error: no match for 'operator==' (operand types are
#     'mylib::point' and 'mylib::point')

Why API_BREAK not BREAKING: there is no symbol to remove and no binary to relink โ€” the .so files are layout-equivalent. The break is entirely at the source layer: a == b has no viable overload once the hidden friend declaration disappears, so the consumer cannot even be recompiled without editing the call site.

Safe redesign

  • Keep the hidden friend (preferred) โ€” it's an ADL contract downstream code relies on.
  • If it genuinely must go, ship a deprecation cycle: keep the inline friend calling a new explicit comparator (equals(a, b)) for one release, then remove.
  • Provide a free function at namespace scope as a migration path: bool operator==(const point&, const point&); declared outside the class.

Real-world example: the hidden-friend idiom is used widely for ADL-only operators (operator==, operator<<, swap) across C++ libraries โ€” oneTBB, oneDAL, Boost, and the standard library all rely on it to avoid polluting the surrounding namespace. Removing one during a "header cleanup" looks binary-safe (and is, at the link layer) but breaks every consumer's next rebuild.

Cross-tool comparison

abidiff/abi-compliance-checker compare binaries (with optional DWARF); neither reads the header AST for in-class friend declarations, so both report no change here โ€” the .so files really are layout-identical. This case is a pure header/API-contract break, which only an L2 (header-aware) tool can see at all.

References


Source files

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

See also: Examples overview ยท All API_BREAK cases ยท Category: API Break.