Hidden Friend Idiom
Define non-member functions as friend inside a class body so they are only visible via ADL, preventing implicit conversions and speeding up overload resolution.
Hidden Friend Idiomsince C++98A non-member function or operator defined inside a class body with the friend keyword, making it only discoverable through argument-dependent lookup (ADL) β invisible to normal unqualified name lookup unless one of its arguments is of the class type.
Overview
A friend function defined inside a class body has two unusual properties: it is a non-member function (not a method), and it is injected into the enclosing namespace but not visible to ordinary unqualified lookup. It can only be found when ADL triggers β that is, when at least one argument of the call has the class type (or a type derived from it).
This "hidden" quality is what gives the idiom its name and its value. Consider the alternative: declaring a comparison operator at namespace scope makes the compiler consider it for every == expression anywhere in a translation unit, regardless of whether the operands are even related to the class. A hidden friend instead stays dormant until a relevant object appears as an argument, which:
- Prevents accidental implicit conversions: the function is not found unless an argument is already the right type, so a type
Yconvertible toXcannot accidentally triggerX's operators when comparing twoYobjects. - Speeds up compilation: fewer candidate functions are gathered during overload resolution, which matters at scale (thousands of
operator==definitions in a large codebase). - Keeps the API surface clean: the function appears to be part of
X's interface without polluting the surrounding namespace for general lookup.
Syntax
// Basic hidden friend pattern
class X {
public:
explicit X(int value) : value_(value) {}
// Defined inside the class body with 'friend' β becomes a hidden friend.
// Found by ADL when either argument is an X; invisible to ::operator== lookup.
friend bool operator==(const X& lhs, const X& rhs) {
return lhs.value_ == rhs.value_;
}
friend bool operator<(const X& lhs, const X& rhs) {
return lhs.value_ < rhs.value_;
}
friend std::ostream& operator<<(std::ostream& out, const X& x) {
return out << x.value_;
}
private:
int value_;
};
// Usage
X a{1}, b{2};
if (a == b) { /* ... */ } // ADL finds operator== β OK
std::cout << a; // ADL finds operator<< β OKExamples
Symmetric Operators Without Implicit-Conversion Leaks
Without hidden friends, a type Y convertible to X would silently pick up X's operators:
namespace lib {
class X {
public:
explicit X(int v) : v_(v) {}
friend bool operator==(const X& a, const X& b) { return a.v_ == b.v_; }
private:
int v_;
};
class Y {
public:
explicit Y(int v) : v_(v) {}
operator X() const { return X(v_); } // convertible to X
private:
int v_;
};
} // namespace lib
lib::X x1{1}, x2{2};
lib::Y y1{1}, y2{2};
x1 == x2; // OK β X's operator== found via ADL
// y1 == y2; // ERROR β operator== is hidden; Y is not X (protected from accidental conversion)
lib::X(y1) == y2; // OK β explicit conversion, then ADL finds operator==Canonical Modern C++ Usage (C++20 operator<=>)
#include <compare>
class Money {
public:
explicit Money(int cents) : cents_(cents) {}
friend auto operator<=>(const Money& a, const Money& b) = default;
friend bool operator==(const Money& a, const Money& b) = default;
friend std::ostream& operator<<(std::ostream& os, const Money& m) {
return os << '$' << m.cents_ / 100 << '.' << m.cents_ % 100;
}
private:
int cents_;
};
Money price{599}, tax{50};
if (price > tax) { /* ... */ } // spaceship generates all six comparisons
std::cout << price; // hidden friend operator<<Hidden Friends via Base Class (CRTP-style mixin)
Hidden friends in a base class are also found for derived types, enabling operator injection through mixins:
template<typename Derived>
struct Addable {
friend Derived operator+(Derived lhs, const Derived& rhs) {
lhs += rhs; // delegates to operator+=
return lhs;
}
};
class Vec2 : public Addable<Vec2> {
public:
Vec2(double x, double y) : x_(x), y_(y) {}
Vec2& operator+=(const Vec2& o) { x_ += o.x_; y_ += o.y_; return *this; }
private:
double x_, y_;
};
Vec2 a{1, 2}, b{3, 4};
Vec2 c = a + b; // hidden friend in Addable<Vec2> found via ADLBest Practices
- Prefer hidden friends for all symmetric operators:
operator==,operator<,operator+,operator<<, etc. The visibility constraint is almost always what you want. - Combine with
= defaultin C++20:friend auto operator<=>(const T&, const T&) = default;andfriend bool operator==(const T&, const T&) = default;is the idiomatic C++20 pattern. - Do not forward-declare hidden friends separately: once a hidden friend is defined in the class body, declaring it again at namespace scope removes the "hidden" property and creates a regular namespace-scope function.
- Use for
swapcustomisation:friend void swap(T& a, T& b) noexcept { ... }makesusing std::swap; swap(a, b);find your overload without polluting the namespace.
Common Pitfalls
Expecting hidden friends to be callable with unqualified names from outside ADL:
struct Point {
int x, y;
friend bool equal(const Point& a, const Point& b) { return a.x == b.x && a.y == b.y; }
};
Point p{1, 2}, q{1, 2};
equal(p, q); // OK β ADL triggered because p is a Point
// equal(1, 2); // ERROR β no ADL, hidden friend not foundAdding a namespace-scope declaration destroys hiddenness:
struct X {
friend void foo(X); // hidden friend (definition or declaration)
};
void foo(X); // β this declaration at namespace scope makes foo() visible everywhere,
// including with implicit conversions; hiddenness is goneQualified calls bypass ADL:
struct X {
friend void process(X) {}
};
X x;
process(x); // OK β ADL finds it
::process(x); // ERROR β qualified lookup, ADL disabledComparison Operators as Hidden Friends
Symmetric comparison operators are the canonical motivation for the hidden friend idiom. When operator== or operator< is defined at namespace scope, every == expression in the translation unit becomes a candidate for overload resolution β including comparisons between completely unrelated types that happen to have an implicit conversion path to your class. Defining them as hidden friends removes them from that general candidate set.
The asymmetry is subtle but important. Suppose operator==(const X&, const X&) is at namespace scope and X has a constructor from int. Then the expression 42 == myX succeeds: the compiler constructs a temporary X(42) and calls the operator. A hidden friend version is unreachable via that path because the int argument carries no association with X's namespace β ADL is not triggered until an object already of type X (or a type derived from it) appears as an argument.
#include <iostream>
class Celsius {
public:
explicit Celsius(double v) : val_(v) {}
// Hidden friend: only found when at least one argument is already a Celsius.
// A bare double cannot trigger this via ADL.
friend bool operator==(const Celsius& a, const Celsius& b) noexcept {
return a.val_ == b.val_;
}
friend bool operator<(const Celsius& a, const Celsius& b) noexcept {
return a.val_ < b.val_;
}
private:
double val_;
};
Celsius boiling{100.0};
// 100.0 == boiling // Would NOT compile β double has no ADL path to Celsius
// boiling == 100.0 // Would NOT compile β same reason (good: prevents silent narrowing)
bool same = boiling == Celsius{100.0}; // OK β both arguments are CelsiusBoth operands must independently be (or be derived from) Celsius; the compiler will not silently construct a Celsius from a raw double just to use the hidden operator. This is precisely the desired behaviour for unit types, strong typedefs, and domain-specific values where implicit promotion is a bug, not a feature.
C++20 Spaceship Operator as a Hidden Friend
The three-way comparison operator operator<=> introduced in C++20 synthesises all six relational operators (==, !=, <, <=, >, >=) from a single definition. When combined with the hidden friend idiom and = default, you get a complete, correct, and ADL-governed comparison suite in two lines.
#include <compare>
#include <string>
class Employee {
public:
Employee(int id, std::string name)
: id_(id), name_(std::move(name)) {}
// Defaulted spaceship: member-wise comparison in declaration order.
// Generates all six operators as hidden friends β none visible to
// ordinary unqualified lookup.
friend auto operator<=>(const Employee& a, const Employee& b) = default;
friend bool operator==(const Employee& a, const Employee& b) = default;
private:
int id_;
std::string name_;
};
Employee alice{1, "Alice"}, bob{2, "Bob"};
bool ordered = alice < bob; // ADL finds hidden operator<=> β OK
bool notEqual = alice != bob; // synthesised from operator== β OKThe = default form delegates to the compiler, which performs member-wise lexicographic comparison using each member's own <=>. The return type is deduced as the common comparison category of all member types β std::strong_ordering here because both int and std::string support it.
A key subtlety: in C++20 you must also explicitly default operator== alongside operator<=>. The standard deliberately does not synthesise operator== from operator<=> for efficiency reasons β equality can often be short-circuited (e.g. checking size first for strings), so the two are kept separate. Defaulting both is the idiomatic pattern.
The spaceship operator inside a class body is injected into the enclosing namespace via the usual friend mechanism, so it is invisible to qualified lookup (MyNS::operator<=>) but reachable through ADL from any context where an Employee argument is present. Generic algorithms in <algorithm> and <ranges> rely on exactly this ADL-based discovery.
Preventing Implicit Conversions on Both Sides
One of the most practically important properties of hidden friends is that they suppress implicit conversions symmetrically β on both the left-hand and right-hand side of a binary operator. This is impossible to achieve with a member function, which always allows implicit construction of the right-hand operand, and difficult to enforce with a namespace-scope non-member.
class Meters {
public:
explicit Meters(double v) : val_(v) {}
// As a member, only the RHS could be implicitly constructed.
// As a hidden friend, NEITHER side allows implicit construction.
friend Meters operator+(const Meters& a, const Meters& b) noexcept {
return Meters{a.val_ + b.val_};
}
private:
double val_;
};
Meters m{5.0};
// m + 3.0 // Does NOT compile β 3.0 has no ADL path to Meters
// 3.0 + m // Does NOT compile β same reason
Meters result = m + Meters{3.0}; // OK β explicit on both sidesContrast this with a namespace-scope operator+: because the function is declared at namespace scope, the compiler considers it whenever + is used anywhere in the translation unit and happily constructs a Meters from a double if the constructor is not explicit. Even with an explicit constructor the compiler could pick up the function and fail with a confusing diagnostic rather than silently doing the wrong thing. The hidden friend version simply does not exist from the perspective of non-ADL lookup, so the error is clean and early.
This pattern is recommended in the C++ Core Guidelines (F.47 and C.161) and in the design of the standard library's std::chrono duration arithmetic, where mixing seconds and milliseconds is intentional but mixing seconds with raw int is not.
The swap Hidden Friend
The two-step swap idiom β using std::swap; swap(a, b); β is the correct way to invoke swap in generic code. It allows user-defined types to provide a custom swap that is faster or more correct than the default (which performs three moves). The hidden friend is the right vehicle for this customisation point.
#include <algorithm> // std::swap fallback
#include <utility>
class Buffer {
public:
explicit Buffer(std::size_t cap)
: data_(new char[cap]), cap_(cap), size_(0) {}
~Buffer() { delete[] data_; }
Buffer(Buffer&& o) noexcept
: data_(o.data_), cap_(o.cap_), size_(o.size_) {
o.data_ = nullptr; o.cap_ = 0; o.size_ = 0;
}
Buffer& operator=(Buffer o) noexcept {
swap(*this, o); // ADL finds the hidden friend below
return *this;
}
// Hidden friend swap: zero-overhead pointer exchange.
// Found by 'using std::swap; swap(a,b)' via ADL.
friend void swap(Buffer& a, Buffer& b) noexcept {
using std::swap;
swap(a.data_, b.data_);
swap(a.cap_, b.cap_);
swap(a.size_, b.size_);
}
private:
char* data_;
std::size_t cap_;
std::size_t size_;
};The noexcept specification is essential. std::vector and other containers check whether swap on the element type is noexcept before deciding whether to use it during reallocation; a throwing swap causes them to fall back to slower, potentially exception-safe alternatives.
In C++20 the std::ranges::swap customisation point object (CPO) follows a priority chain: first it checks if the arguments are arrays, then looks for a hidden-friend or ADL-reachable swap with the right signature, and finally falls back to std::swap. The hidden-friend swap above is found at step two β it integrates seamlessly with the Ranges machinery without any additional work. Because std::ranges::swap is a Niebloid (an object that suppresses ADL for its own name), it does not itself participate in ADL; instead it drives the lookup internally, so your hidden friend must be reachable from the type's associated namespace, which it is by definition.
operator<< as a Hidden Friend
Stream insertion is the third canonical use of hidden friends alongside comparison and swap. Defining operator<< as a namespace-scope function requires either a forward declaration separate from the class or placement after the full class definition, both of which are awkward. The hidden friend definition places the implementation next to the private data it accesses without requiring friend re-declarations elsewhere.
#include <ostream>
#include <iomanip>
class Temperature {
public:
enum class Scale { Celsius, Fahrenheit, Kelvin };
Temperature(double v, Scale s) : val_(v), scale_(s) {}
friend std::ostream& operator<<(std::ostream& os, const Temperature& t) {
os << t.val_;
switch (t.scale_) {
case Scale::Celsius: return os << " Β°C";
case Scale::Fahrenheit: return os << " Β°F";
case Scale::Kelvin: return os << " K";
}
return os;
}
private:
double val_;
Scale scale_;
};
Temperature boiling{100.0, Temperature::Scale::Celsius};
std::cout << boiling << '\n'; // ADL on second argument finds hidden friendThe std::ostream argument is what makes this case slightly different: the first argument is from std::, not from your namespace, so ADL on the left operand finds candidates in namespace std. The second argument β the Temperature β triggers ADL in Temperature's associated namespace, which is where the hidden friend lives. Both sets of candidates are merged, the hidden friend wins on the second argument, and the call resolves correctly.
The operator>> (stream extraction) follows the identical pattern with std::istream& as the first argument and a non-const reference as the second.
Interaction with Ranges and Customisation Point Objects
C++20 Ranges introduced Niebloids β function objects like std::ranges::begin, std::ranges::end, std::ranges::swap, and std::ranges::size β that are designed to suppress ordinary ADL on their own name. A Niebloid is not a function; it is a callable object, so an unqualified call begin(r) where begin has been brought in via using std::ranges::begin does not invoke ADL lookup of begin in the same way a free function call would. This is intentional: it prevents accidental hijacking by unrelated begin overloads found via ADL.
However, this does not mean hidden friends are bypassed. The Niebloids implement their own internal lookup strategy:
std::ranges::begin(r):
1. If r is an array β use pointer arithmetic.
2. Else if r.begin() is valid β use the member function.
3. Else if begin(r) found via ADL in r's associated namespaces β use it.
4. Otherwise β ill-formed.Step 3 is exactly where a hidden-friend begin is found. So for a custom range type, the following works correctly:
#include <ranges>
#include <iterator>
class IntSpan {
public:
IntSpan(int* p, std::size_t n) : ptr_(p), len_(n) {}
// Hidden friends for begin/end β found by std::ranges::begin/end
// at step 3 of their internal ADL fallback.
friend int* begin(IntSpan& s) noexcept { return s.ptr_; }
friend int* end(IntSpan& s) noexcept { return s.ptr_ + s.len_; }
friend const int* begin(const IntSpan& s) noexcept { return s.ptr_; }
friend const int* end(const IntSpan& s) noexcept { return s.ptr_ + s.len_; }
private:
int* ptr_;
std::size_t len_;
};
int arr[] = {3, 1, 4, 1, 5};
IntSpan span{arr, 5};
// All of these find the hidden friends through the Niebloid's ADL fallback:
auto b = std::ranges::begin(span);
auto e = std::ranges::end(span);
for (int x : span) { /* range-for works too */ }The distinction between Niebloids (which suppress ADL on their own call) and the ADL fallback inside them (which does search associated namespaces) is one of the subtler aspects of C++20. The practical conclusion for hidden friends is reassuring: they remain fully compatible with std::ranges algorithms. What Niebloids prevent is the old using std::begin; begin(r); ambiguity problem, not the discovery of your customisation points.
std::ranges::swap follows the same three-step priority internally and finds hidden-friend swap overloads at step two. std::ranges::iter_swap and std::ranges::iter_move similarly delegate to ADL-found customisation points, so hidden friends propagate correctly through the entire Ranges machinery.
See Also
ADL Customizationβ the broader pattern of using argument-dependent lookup for extensible generic algorithmsOperator Overloadingβ guidelines on symmetric vs asymmetric operator placementCRTPβ a complementary pattern for injecting behaviour through base classes