This guide explains Friend Class in C++ in a simple, beginner-friendly way. You’ll learn what they are, when to use them, and how to structure your code safely. Every section includes “Try it yourself” challenges to reinforce learning.
Think of a friend class as a trusted workshop with a master key. You explicitly grant it access to your class’s private/protected members so it can perform specialized tasks like building, auditing, or serializing—without exposing those details to everyone.
🔹 What is Friend Class in C++?
A friend class is allowed to access another class’s private and protected members. You grant this access by declaring friend class Name;
inside the class offering access. Use this when two classes are tightly coupled (e.g., builder, inspector, serializer).
- Friendship is granted, not taken.
- Not inherited, not transitive, not reciprocal.
- Use sparingly to avoid tight coupling.
Try it yourself
- Explain why a friend class may be better than exposing many getters.
- Pick two roles (e.g.,
Car
andCarService
) and decide why service might need special access.
🔹 Basic Syntax and First Example
Declare friendship inside the class that grants access. The friend class can then read or modify private/protected members directly.
#include <iostream>
#include <string>
using namespace std;
// forward declaration (optional here)
class Auditor;
class BankAccount {
private:
string owner;
double balance;
bool frozen{false};
public:
BankAccount(string name, double initial) :
owner(std::move(name)), balance(initial) {}
double getBalance() const { return balance; }
// Grant Auditor privileged access
friend class Auditor;
};
class Auditor {
public:
void report(const BankAccount& acc) const {
cout << "[AUDIT] Owner: " << acc.owner
<< ", Balance: " << acc.balance
<< ", Frozen: " <<
(acc.frozen ? "yes" : "no") << "\n";
}
void freeze(BankAccount& acc) const
{ acc.frozen = true; } // direct private access
};
int main() {
BankAccount a("Ria", 1200.50);
Auditor audit;
audit.report(a);
audit.freeze(a);
audit.report(a);
}
Output
[AUDIT] Owner: Ria, Balance: 1200.5, Frozen: no
[AUDIT] Owner: Ria, Balance: 1200.5, Frozen: yes
Try it yourself
- Create a non-friend
CustomerService
and try to printowner
—observe the error. - Add an
unfreeze
method toAuditor
and test it.
🔹 Access Rules: Scope and Limits
Friendship is precise: a friend of a base class can access the base subobject in derived instances, but it is not automatically a friend of the derived class’s own members.
#include <iostream>
using namespace std;
class Base;
class Inspector {
public:
void peek(const Base& b);
};
class Base {
private:
int secret{42};
protected:
int semi{7};
public:
friend class Inspector;
};
class Derived : public Base {
private:
// Inspector cannot access this unless Derived also befriends it
int extra{99};
};
void Inspector::peek(const Base& b) {
cout << b.secret << " "
<< b.semi << "\n";
}
int main() {
Derived d;
Inspector ins;
ins.peek(d); // accesses Base part inside Derived
}
Output
42 7
Try it yourself
- Add a method in
Inspector
that tries to readDerived::extra
. See the error. - Add
friend class Inspector;
toDerived
and try again.
🔹 Friend Class vs. Friend Function
Friend functions grant access to one function only (narrow). Friend class grant access to all of the friend’s methods (broader). Prefer the narrowest access that fits your design.
#include <iostream>
using namespace std;
class Data {
private:
int value{10};
public:
friend void print(const Data& d); // narrow access
friend class Helper; // broad access
};
void print(const Data& d)
{ cout << "Value: " << d.value << "\n"; }
class Helper {
public:
void doubleIt(Data& d) { d.value *= 2; }
void reset(Data& d) { d.value = 0; }
};
int main() {
Data d;
print(d);
Helper h;
h.doubleIt(d);
print(d);
h.reset(d);
print(d);
}
Output
Value: 10
Value: 20
Value: 0
Try it yourself
- Remove
friend class Helper;
and instead friend only two free functionsdoubleIt
andreset
. Which design is clearer?
🔹 Forward Declarations and Header Hygiene
When befriending a class declared in another header, forward-declare it to avoid circular includes. Document why the friendship exists.
#include <string>
using namespace std;
// Forward declare the friend class
class ConfigEditor;
class Config {
private:
string key, value;
public:
Config(string k, string v) :
key(std::move(k)), value(std::move(v)) {}
// Friend declared using the forward declaration
friend class ConfigEditor;
};
class ConfigEditor {
public:
void set(Config& c, string k, string v)
{ c.key = std::move(k); c.value = std::move(v); }
};
Try it yourself
- Split into
config.hpp
andconfig_editor.hpp
, use forward declarations, and compile both. - Add a comment above the
friend
line explaining its purpose (e.g., “editor needs full write access”).
🔹 Pattern: Builder with Friend Class (Immutable Object)
Use a private constructor to enforce invariants and let a friend builder construct valid objects. The built object can expose a read-only API.
#include <iostream>
#include <string>
using namespace std;
class UserBuilder; // forward declare
class User {
private:
string name, email;
int age;
// Private constructor: only the builder can call this
User(string n, string e, int a) :
name(std::move(n)), email(std::move(e)), age(a) {}
public:
const string& getName() const { return name; }
const string& getEmail() const { return email; }
int getAge() const { return age; }
friend class UserBuilder; // builder requires construction access
};
class UserBuilder {
private:
string name, email;
int age{0};
public:
UserBuilder& setName(string n)
{ name = std::move(n); return *this; }
UserBuilder& setEmail(string e)
{ email = std::move(e); return *this; }
UserBuilder& setAge(int a)
{ age = a; return *this; }
User build() const {
if (name.empty())
throw runtime_error("Name required");
if (age < 0)
throw runtime_error("Age must be non-negative");
return User(name, email, age); // allowed via friendship
}
};
int main() {
User u = UserBuilder().setName("Aman").setEmail("a@ex.com").setAge(24).build();
cout << u.getName() << ", " << u.getEmail()
<< ", " << u.getAge() << "\n";
}
Output
Aman, a@ex.com, 24
Try it yourself
- Add optional
phone
with a default if not set. - Enforce a valid email format in
build()
(simple check like “contains @”).
🔹 Pattern: Inspector/Serializer as a Friend Class
Keep data private but allow a dedicated friend class to read internal state for formats like CSV/JSON or debug strings, without bloating the main type’s public interface.
#include <iostream>
#include <sstream>
#include <string>
using namespace std;
class Order;
class OrderSerializer;
class Order {
private:
int id;
string item;
double price;
bool paid{false};
public:
Order(int i, string it, double p) :
id(i), item(std::move(it)), price(p) {}
void markPaid() { paid = true; }
// serializer needs full read access
friend class OrderSerializer;
};
class OrderSerializer {
public:
string toCSV(const Order& o) const {
stringstream ss;
ss << o.id << "," << o.item
<< "," << o.price << ","
<< (o.paid ? "yes" : "no");
return ss.str();
}
string toDebugString(const Order& o) const {
stringstream ss;
ss << "Order{id=" << o.id << ", item="
<< o.item << ", price=" << o.price
<< ", paid=" << (o.paid ? "true" : "false")
<< "}";
return ss.str();
}
};
int main() {
Order o(101, "Keyboard", 2499.00);
o.markPaid();
OrderSerializer ser;
cout << ser.toCSV(o) << "\n";
cout << ser.toDebugString(o) << "\n";
}
Output
101,Keyboard,2499,yes
Order{id=101, item=Keyboard, price=2499, paid=true}
Try it yourself
- Add
static Order fromCSV(const string&)
toOrderSerializer
to parse a line. - Try a non-friend serializer that uses only public getters—what do you gain/lose?
🔹 Templates and Friend Class
You can friend a specific template specialization or all specializations. Choose the narrowest access that fits.
#include <iostream>
using namespace std;
template <typename T> class MatrixOps; // forward declare
template <typename T>
class Matrix {
private:
int rows, cols;
T* data;
public:
Matrix(int r, int c) : rows(r), cols(c), data(new T[r*c]{}) {}
~Matrix() { delete[] data; }
// Friend only the matching specialization
friend class MatrixOps<T>;
// Or friend all: template <typename U> friend class MatrixOps;
};
template <typename T>
class MatrixOps {
public:
void fill(Matrix<T>& m, const T& v) {
for (int i = 0; i < m.rows*m.cols; ++i) m.data[i] = v;
}
T sum(const Matrix<T>& m) const {
T s{};
for (int i = 0; i < m.rows*m.cols; ++i) s += m.data[i];
return s;
}
};
int main() {
Matrix<int> m(2, 3);
MatrixOps<int> ops;
ops.fill(m, 5);
cout << ops.sum(m) << "\n"; // 30
}
Output
30
Try it yourself
- Switch to friending all specializations and observe that
MatrixOps<double>
can now accessMatrix<int>
internals (if you adjust the code). - Write a non-friend operation using only a minimal public API and compare complexity.
🔹 Testing and Conditional Friendship
Sometimes tests need to inspect internals. You can grant test-only friendship behind a macro so production builds don’t expose it.
#include <string>
using namespace std;
class TestHooks; // forward declare
class Token {
private:
string raw;
int type{0};
public:
explicit Token(string r) : raw(std::move(r)) {}
#ifdef ENABLE_TESTS
friend class TestHooks; // test-only friend
#endif
};
#ifdef ENABLE_TESTS
class TestHooks {
public:
static string raw(const Token& t) { return t.raw; }
static int type(const Token& t) { return t.type; }
};
#endif
Try it yourself
- Build with
-DENABLE_TESTS
and access internals viaTestHooks
; then build without it and confirm it’s unavailable. - Prefer behavior-based tests via public APIs where possible.
🔹 Best Practices and Common Pitfalls
- Document the reason above each
friend
(e.g., “builder needs construction access”). - Prefer friend functions for narrow, single operations; friend classes for cohesive helper roles.
- Minimize coupling—don’t use friends as a shortcut for weak design.
- Use forward declarations to avoid circular includes.
- Remember: no virtual friends; friendship is a compile-time access rule.
- Keep friend helpers small, focused, and well-tested.
Try it yourself
- Audit one class and remove an unnecessary friend by adding a narrow getter or a DTO.
- Split an overly broad friend helper into smaller, focused helpers.
🔹 Quick Reference: Common Patterns
// Builder friend (private constructor)
class A { int x; A(int v) : x(v) {} friend class ABuilder; };
// Serializer/Inspector friend
class B { int hidden; friend class BSerializer; };
// Friend function (narrow access)
class C { int v; friend void print(const C&); };
// Template friend (specific specialization)
template <typename T> class Ops;
template <typename T> class D { T* data; friend class Ops<T>; // or: template <typename U> friend class Ops; };
🔹 FAQs about Friend Class in C++
Q1: Do friend class break encapsulation?
They can if overused. Use them deliberately for builders, serializers, inspectors, etc. Try it: replace one friend with a minimal getter and reassess.
Q2: Is friendship inherited or transitive?
No on both counts. Try it: add a derived class and observe you still need to befriend it explicitly.
Q3: Should I prefer friend class or friend functions?
Prefer friend functions for single, precise operations; use friend class when a cohesive helper needs full access. Try it: split a friend class into two friend functions and compare clarity.
Q4: Can a friend class access static private members?
Yes—friends have the same access as members to private/protected state, including statics. Try it: add a private static counter and read it from a friend.
Q5: Does friendship have runtime cost?
No, it’s a compile-time access rule. Performance depends on your algorithms and data structures, not friendship. Try it: benchmark friend vs non-friend serializers.
🔹 Summary
Friend Class in C++ let you grant intentional, fine-grained access to trusted helpers while keeping a clean public interface. Use them sparingly, document intent, prefer narrow access when possible, and keep helpers focused. With the examples and “Try it yourself” tasks above, you’re ready to use friend class confidently and safely.