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 | browse on GitHub |
Category: Source API contract | Verdict: ๐ API_BREAK
What breaks¶
A non-member operator (operator==) was declared as a hidden friend
inside the class body โ i.e. as an in-class friend declaration with an
inline definition. Hidden friends are findable only via argument-dependent
lookup (ADL); they have
no namespace-scope declaration that consumers can name. When the
declaration is removed in v2, every consumer that wrote a == b against
v1's header fails to compile against v2's. The library's .so is byte-
identical (the inline friend never had a public symbol to remove), so the
break is invisible to any binary-only diff tool.
Why this is a oneTBB-flavored break¶
Hidden friends are the idiomatic C++17+ way to declare ADL-only
operators on a type (operator==, operator<<, swap, etc.) without
polluting the surrounding namespace. oneTBB, oneDAL, Boost, and the
standard library all use them extensively. Removing one looks like a
"cosmetic header cleanup" that the maintainer believes is binary-safe โ
which it is, at the link layer. It's the source-recompile that explodes.
How abicheck catches it¶
New ChangeKind HIDDEN_FRIEND_REMOVED (API_BREAK). The dumper reads
castxml's befriending attribute on the Class / Struct element โ
a whitespace-separated list of ids that point to the function elements
that were declared as in-class friend. Each such function is marked
is_hidden_friend=True in the snapshot. The diff scans for hidden
friends present in v1 but absent in v2 and emits HIDDEN_FRIEND_REMOVED.
The symmetric kind HIDDEN_FRIEND_ADDED is COMPATIBLE (pure addition;
existing code keeps compiling, new operator only participates at call
sites that trigger ADL).
Complementary findings on out-of-line friends¶
If the hidden friend was also defined out-of-line (so it has a real
exported symbol), removal additionally fires FUNC_REMOVED at the
binary layer. Both findings are emitted โ the API_BREAK reflects the
source-level ADL break and the BREAKING reflects the link-level break.
Tri-state on is_hidden_friend¶
DWARF-only snapshots and any snapshot produced by a dumper that predates
this field set is_hidden_friend=None. The transition detector (in
_check_function_signature) skips when either side is None, mirroring
the explicit-ctor detector's handling of schema evolution.
Code diff¶
| v1 | v2 |
|---|---|
friend bool operator==(...) { ... } inside point |
declaration removed |
Class befriending="_34" in castxml output |
befriending attribute absent |
a == b compiles for any consumer |
a == b fails to compile |
Real Failure Demo¶
Severity: API BREAK (source-only โ binaries keep linking)
# v1 header, v1 .so: compiles and runs.
g++ -std=c++17 -I. app.cpp -L. -lmylib -o app
./app # โ a == b โ true (expect true)
# v2 header, v2 .so: app.cpp does `bool eq = (a == b);`, which
# resolves via ADL to the hidden friend `operator==` of `mylib::point`.
# v2 removes the friend declaration; ADL has nothing to find, so the
# same app.cpp source line no longer compiles:
g++ -std=c++17 -I. app.cpp -L. -lmylib -o app
# โ error: no match for 'operator==' (operand types are 'mylib::point'
# and 'mylib::point')
How to fix¶
- Keep the hidden friend (preferred) โ it's an ADL contract that downstream code relies on.
- If you genuinely need to remove it, ship a deprecation cycle:
inline-define it to call 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&);outside the class.
References¶
- C++17 hidden friend idiom โ Walter Brown, "Hidden Friends"
- castxml output โ
befriendingattribute on Class/Struct - cppreference: argument-dependent lookup
Source files¶
See also: Examples overview ยท All API_BREAK cases ยท Category: API Break.