C++ / CLASSES AND OBJECT LIFECYCLE
Friend functions and classes, used sparingly
Grant private access to specific functions or classes with friend, use it for stream and symmetric operators, and know when an accessor is the better fix.
What you will learn
- Grant private access to one named function or class with a friend declaration
- Write operator<< and symmetric arithmetic as non-members that reach private data
- Define a hidden friend in the class body and call it via argument-dependent lookup
- Decide when a public accessor makes a friend declaration unnecessary
Understanding Friend functions and classes, used sparingly
A friend declaration is a grant the class writes about someone else: it names a function or a class and states that this code may read the private and protected members. Because it is a grant and not a member, the surrounding public: or private: label has no effect on it, and the friend never becomes a member itself, so it has no this pointer, cannot be const-qualified, and is called as a free function. The grant is one-directional and narrow: befriending SessionManager says nothing about SessionManager's own privates, nothing about classes derived from either side, and nothing about SessionManager's friends.
The situations that justify friendship are few. A binary operator whose left operand is not your class, such as operator<< with an std::ostream on the left, cannot be a member at all, and the same asymmetry shows up in arithmetic: a member operator+ never converts its implicit object argument, so 0.25 + d cannot work while d + 0.25 does. The other honest case is a pair of types designed and changed together, like a container and its iterator, or a factory and a product whose constructor is deliberately private. Defining the friend inside the class body gives you a hidden friend: a non-member that ordinary lookup cannot see and that is found only through the types of its arguments.
Judge encapsulation by counting the code that must change if you alter the private members. A friend adds one named, reviewable entry to that count, which is often a smaller commitment than a public getter that anyone may call forever, so friend is not automatically a hole in the class. It turns bad when it is coarse or lazy: befriending a whole class hands every present and future member of it access to your representation, and befriending a function that only touches public members is pure noise that should be deleted.
<iostream>
class Money {
public:
Money(long long dollars, int cents) : cents_(dollars * 100 + cents) {}
// Defined inside the class, yet still a non-member: no this, no Money:: prefix.
friend std::ostream& operator<<(std::ostream& os, const Money& m) {
return os << '$' << m.cents_ / 100 << '.'
<< (m.cents_ % 100 < 10 ? "0" : "") << m.cents_ % 100;
}
private:
explicit Money(long long cents) : cents_(cents) {}
long long cents_;
// A friend grant ignores access labels: sitting under private: changes nothing.
friend Money operator+(const Money& a, const Money& b);
};
Money operator+(const Money& a, const Money& b) {
return Money(a.cents_ + b.cents_); // private constructor and private field
}
int main() {
Money rent(1200, 50);
Money fee(75, 5);
std::cout << rent << " + " << fee << " = " << rent + fee << '\n';
}
friend is a targeted grant the class issues to named outside code, extending its implementation instead of opening it to everyone.
Worked examples
A factory holding the only key
One class is befriended so it becomes the sole path for creating and modifying another.
<iostream>
<string>
<utility>
class Session {
public:
void log() const { std::cout << "session " << id_ << " for " << user_ << '\n'; }
private:
Session(int id, std::string user) : id_(id), user_(std::move(user)) {}
int id_;
std::string user_;
friend class SessionManager;
};
class SessionManager {
public:
Session open(const std::string& user) { return Session(next_id_++, user); }
void rename(Session& s, const std::string& user) { s.user_ = user; }
private:
int next_id_ = 1;
};
int main() {
SessionManager mgr;
Session a = mgr.open("ada");
Session b = mgr.open("linus");
a.log();
b.log();
mgr.rename(b, "linus_t");
b.log();
// Session c(99, "eve"); // error: constructor is private, main is not a friend
}
Example explained
Line 1friend class SessionManager; lets every member of the manager reach Session's privates, including its private constructor.
Line 2open() can call Session(next_id_++, user) only because of that grant, which is what makes the manager the single construction path.
Line 3rename() assigns s.user_ directly, so Session needs no public setter at all.
Line 4The grant does not run backwards: Session cannot read SessionManager::next_id_.
Hidden friend swap
A friend defined in the class body is a symmetric non-member reachable only through its arguments.
<iostream>
<utility>
class Buffer {
public:
explicit Buffer(int n) : size_(n), data_(new int[n]()) {}
~Buffer() { delete[] data_; }
Buffer(const Buffer&) = delete;
Buffer& operator=(const Buffer&) = delete;
int size() const { return size_; }
friend void swap(Buffer& a, Buffer& b) {
std::swap(a.size_, b.size_);
std::swap(a.data_, b.data_);
}
private:
int size_;
int* data_;
};
int main() {
Buffer small(2), big(8);
std::cout << small.size() << ' ' << big.size() << '\n';
swap(small, big); // unqualified: argument-dependent lookup finds the friend
std::cout << small.size() << ' ' << big.size() << '\n';
// ::swap(small, big); // error: no swap declared at global scope
}
Example explained
Line 1swap takes both buffers as ordinary parameters, so neither side is privileged; a member swap would treat one as this.
Line 2The name swap never enters the global scope, so only a call with a Buffer argument finds it through lookup on the argument type.
Line 3The body exchanges size_ and data_ directly, which is genuine private access and the reason the friend grant is needed.
Line 4Copying is deleted, so a swap written with a temporary copy could not compile; swapping the raw members is the whole point.
Both operand orders
A non-member operator lets the converting constructor apply on the left as well as the right.
<iostream>
class Meters {
public:
Meters(double v) : v_(v) {} // converting constructor, on purpose
double value() const { return v_; }
friend Meters operator+(Meters a, Meters b) { return Meters(a.v_ + b.v_); }
private:
double v_;
};
int main() {
Meters d(2.5);
std::cout << (d + 0.25).value() << '\n'; // right operand converts
std::cout << (0.25 + d).value() << '\n'; // left operand converts too
}
Example explained
Line 1Meters(double) allows a plain double to become a Meters wherever a Meters parameter is expected.
Line 20.25 + d compiles because both operands are ordinary parameters; a member operator+ would reject it, since the implicit object argument is never converted.
Line 3The body reads a.v_ directly, but value() would have worked just as well, so the essential property here is being a non-member, not being a friend.
Line 4Writing it in the class body keeps the name out of the global scope and out of overload resolution for unrelated types.
Important notes
friend class Manager; grants access to every current and future member of Manager, so prefer naming the one function that needs it.
You can even befriend a single member of another class, as in friend Session SessionManager::open(const std::string&);, but that requires SessionManager to be declared above, which usually means a forward declaration plus an out-of-line definition.
Common mistakes
Treating the friend as a member: adding const after the parameter list, defining it as Money::operator+, or calling it as m.operator+(n). All three fail to compile because a friend has no this pointer.
Assuming friendship spreads. Befriending a base class gives derived classes nothing, and a friend's own friends get nothing either, so those accesses are rejected even though the friend declaration is right there.
Declaring a friend only inside the class and then calling it without an argument of that class type, such as a factory taking no parameters. Ordinary lookup cannot see the name, so the compiler reports it as not declared in this scope.
Try it yourself
Change, predict, then run
Write a Fraction class with private num_ and den_, a hidden friend operator<< that prints 3/4, and a friend bool equal(Fraction, Fraction) that compares a.num_ * b.den_ with b.num_ * a.den_, then confirm equal(Fraction(1, 2), Fraction(2, 4)) is true. Now add public num() and den() accessors and decide which of the two friend declarations you can delete.
Open the C++ workspaceCheck your understanding
A Meters class stores one private double, has a converting constructor from double, and already exposes a public value(). You need both d + 0.25 and 0.25 + d to compile. Which design fits best?
- A member operator+(Meters) const, since the compiler will convert the left operand when needed
- Member overloads for both operator+(Meters) and operator+(double)
- A non-member operator+(Meters, Meters) implemented with value(), no friend declaration
- A friend operator+(Meters, Meters) declared inside Meters so the body can read the private field
Show answer
Only a non-member puts both operands in positions where the converting constructor can apply, so options 0 and 1 reject 0.25 + d: the implicit object argument of a member function is never converted, no matter how many overloads you add. Option 3 is tempting and does compile identically, but value() already publishes the data, so the grant adds code that depends on the private layout for no benefit.