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¶
- C++17 hidden friend idiom โ Walter Brown, "Hidden Friends"
- castxml output โ
befriendingattribute on Class/Struct - cppreference: argument-dependent lookup
Source files¶
CMakeLists.txtapp.cppv1.cppv1.hv2.cppv2.h
See also: Examples overview ยท All API_BREAK cases ยท Category: API Break.