C++ friend Keyword: Access Control and Operators

Key takeaways

friend is often called a hole in encapsulation, but for symmetric operators on a class like Fraction it is the cleaner choice. The post compares friend functions with members and getters, shows friend classes and friend member functions, and lists mistakes such as assuming friendship is inherited.

What is friend?

By default, private members of a class are accessible only by the class’s own member functions. The friend keyword grants another function or class permission to access those private members:

class Box {
    int width_;  // private

public:
    Box(int w) : width_(w) {}

    // Declare printWidth as a friend — it can access width_
    friend void printWidth(const Box& box);
};

// Not a member, but has friend access to Box's private members
void printWidth(const Box& box) {
    std::cout << "Width: " << box.width_ << '\n';  // OK — friend access
}

int main() {
    Box b(42);
    printWidth(b);  // prints: Width: 42
}

The friend declaration lives inside the class definition, but the friend function itself is not a member — it’s a regular free function with special access. It has no this pointer, it is not affected by the public: / private: section it appears in (a friend declaration under private: grants exactly the same access as one under public:), and it is called like any other function: printWidth(b), never b.printWidth().

Access control in C++ is checked at compile time, per class. friend does not change the object layout or add any runtime cost; it only tells the compiler “this particular function is allowed to name width_”. That is why it is a design tool rather than a performance one — the question is always who should be allowed to depend on a class’s internals, since every friend is code that must be revisited when those internals change.


Friend Functions vs Member Functions

The choice between a friend function and a member function matters for operators:

class Vector2D {
    double x_, y_;

public:
    Vector2D(double x, double y) : x_(x), y_(y) {}

    // Member function: first operand must be a Vector2D
    Vector2D operator+(const Vector2D& other) const {
        return {x_ + other.x_, y_ + other.y_};
    }

    // Member function: can access private fields directly
    double length() const {
        return std::sqrt(x_ * x_ + y_ * y_);
    }

    // Friend non-member: both operands treated symmetrically
    friend Vector2D operator*(double scalar, const Vector2D& v) {
        return {scalar * v.x_, scalar * v.y_};
    }

    // operator<< must be non-member (ostream is on the left)
    friend std::ostream& operator<<(std::ostream& os, const Vector2D& v) {
        return os << '(' << v.x_ << ", " << v.y_ << ')';
    }
};

int main() {
    Vector2D a(1, 2), b(3, 4);

    auto sum = a + b;                // member operator+: OK
    auto scaled = 2.0 * a;          // friend operator*: OK (2.0 on left)
    // auto scaled2 = a * 2.0;      // also works if you add that overload

    std::cout << sum << '\n';        // calls friend operator<<
    std::cout << a.length() << '\n'; // member function
}

Rule: if the left operand is not your class type (or you want symmetric treatment), make it a non-member friend. operator<< is the most common case — ostream is always on the left.

The operator* and operator<< here are defined inside the class body. Such a function is called a hidden friend: it is a non-member function in the enclosing namespace, but it is not visible to ordinary name lookup — it can only be found through argument-dependent lookup, i.e. when one of the arguments is a Vector2D. That is a feature, not a quirk. It keeps these operators out of overload resolution for unrelated types (which speeds up compilation and shortens error messages when some other operator<< call fails), and it prevents implicit conversions from accidentally selecting them. The C++ standard library increasingly uses hidden friends for comparison operators for the same reasons.

The flip side is that a hidden friend cannot be called with explicit qualification or taken by address without a separate namespace-scope declaration. If you write friend void helper(); with no parameters of the class type and define it in the class, nothing can ever call it — there is no argument to trigger ADL, and the compiler reports “‘helper’ was not declared in this scope” at the call site.


Friend Classes

A friend class gets access to all private and protected members of the grantor:

class Engine;  // forward declaration

class Car {
    int fuel_;
    int speed_;

    friend class Engine;  // Engine can access Car's private members

public:
    Car() : fuel_(100), speed_(0) {}
    int speed() const { return speed_; }
};

class Engine {
public:
    void accelerate(Car& car, int amount) {
        if (car.fuel_ > 0) {           // friend access — reads private fuel_
            car.speed_ += amount;      // friend access — modifies private speed_
            car.fuel_ -= amount / 10;  // friend access — modifies private fuel_
        }
    }

    int fuel(const Car& car) const {
        return car.fuel_;  // friend access
    }
};

int main() {
    Car car;
    Engine engine;

    engine.accelerate(car, 30);
    std::cout << "Speed: " << car.speed() << ", Fuel: " << engine.fuel(car) << '\n';
}

Friend classes represent a stronger coupling than friend functions. All methods of Engine can see all private members of Car. This is appropriate when the two classes are tightly coupled by design (e.g., iterator and container).


Friend Member Functions

You can grant friendship to a specific member function of another class (not the whole class):

class Server;  // forward declaration is enough for Logger's declarations

class Logger {  // must be defined before Server names Logger::logStatus
public:
    void logStatus(const Server& s);
    void otherMethod(const Server& s);
};

class Server {
    int port_;
    std::string secret_key_;

    // Only Logger::logStatus can access Server's private members
    friend void Logger::logStatus(const Server&);

public:
    Server(int port) : port_(port), secret_key_("abc123") {}
};

// Defined after Server is complete, so s.port_ is usable
void Logger::logStatus(const Server& s) {
    std::cout << "Port: " << s.port_ << '\n';
    // std::cout << s.secret_key_;  // would compile, but we choose not to log it
}

void Logger::otherMethod(const Server& s) {
    // std::cout << s.port_;  // compile error — not a friend function
}

This is more precise than making the whole class a friend, but it forces a specific ordering: Logger must be fully defined before Server can befriend one of its members (with only class Logger; the compiler rejects Logger::logStatus as a member of an incomplete type), while the body of logStatus must come after Server is complete so it can see port_. That ordering is why this form is rarer in practice than friend classes — once the two classes live in separate headers, it introduces an include-order dependency that is easy to break.


Factory Functions Using Friend

Private constructors + friend factory is a pattern to control how objects are created:

class SecureToken {
    std::string value_;

    // Private constructor — cannot be called directly
    explicit SecureToken(std::string val) : value_(std::move(val)) {}

    friend SecureToken createToken(const std::string& key);

public:
    const std::string& value() const { return value_; }
};

SecureToken createToken(const std::string& key) {
    // Validate key, generate token, etc.
    if (key.empty()) throw std::invalid_argument("Key required");
    return SecureToken{"tok_" + key};  // friend — can call private constructor
}

int main() {
    // SecureToken t("raw");  // compile error — constructor is private
    auto token = createToken("mykey");  // must go through factory
    std::cout << token.value() << '\n';  // tok_mykey
}

The alternative is a static member function (SecureToken::create(key)), which also has access to the private constructor without any friend at all, and is the more common choice. A friend factory makes sense when the factory naturally lives elsewhere — for example, a TokenService class whose method needs to mint tokens.

One well-known limitation: a private constructor blocks std::make_unique<SecureToken>(...) and std::make_shared, even when called from inside the friend factory, because the actual constructor call happens inside the standard library, which is not a friend. The error points deep into <memory> (“calling a private constructor of class ‘SecureToken’”). Befriending std::make_unique is not portable, since the construction may happen in an implementation-detail helper. The usual workarounds are std::unique_ptr<SecureToken>(new SecureToken(...)) inside the factory, or the “passkey” idiom: a public constructor that takes a private tag type only the factory can create.


A Fraction class with friend arithmetic operators

#include <cstdlib>
#include <numeric>
#include <stdexcept>
#include <iostream>

class Fraction {
    int num_, den_;

    void reduce() {
        int g = std::gcd(std::abs(num_), den_);
        num_ /= g;
        den_ /= g;
    }

public:
    Fraction(int num, int den) : num_(num), den_(den) {
        if (den_ == 0) throw std::invalid_argument("Denominator cannot be zero");
        if (den_ < 0) { num_ = -num_; den_ = -den_; }
        reduce();
    }

    // Arithmetic as non-member friends — symmetric, natural syntax
    friend Fraction operator+(const Fraction& a, const Fraction& b) {
        return {a.num_ * b.den_ + b.num_ * a.den_, a.den_ * b.den_};
    }

    friend Fraction operator*(const Fraction& a, const Fraction& b) {
        return {a.num_ * b.num_, a.den_ * b.den_};
    }

    friend bool operator==(const Fraction& a, const Fraction& b) {
        return a.num_ == b.num_ && a.den_ == b.den_;
    }

    friend std::ostream& operator<<(std::ostream& os, const Fraction& f) {
        if (f.den_ == 1) return os << f.num_;
        return os << f.num_ << '/' << f.den_;
    }
};

int main() {
    Fraction half(1, 2), third(1, 3);
    std::cout << half + third << '\n';   // 5/6
    std::cout << half * third << '\n';   // 1/6
    std::cout << (half == Fraction(2, 4)) << '\n';  // 1 (true — 1/2 == 2/4)
}

Because the constructor always normalizes (positive denominator, reduced by the GCD), operator== can compare the fields directly — 2/4 is stored as 1/2. That invariant is the real reason the operators need private access: they construct results through the constructor, which re-establishes it, and they rely on it when comparing.

The symmetry argument becomes concrete if you give the constructor a default denominator, Fraction(int num, int den = 1). The single-argument form then acts as an implicit conversion from int, and with non-member operators both half + 1 and 1 + half compile, because the compiler may convert either argument. With a member operator+, only half + 1 works; 1 + half fails with “no match for ‘operator+’ (operand types are ‘int’ and ‘Fraction’)”, since implicit conversions are never applied to the object a member function is called on.

A production version would also guard against overflow: a.num_ * b.den_ overflows int for moderately large fractions, and signed overflow is undefined behavior. Reducing by the GCD of the denominators before multiplying, or using long long intermediates, pushes the limit much further.


friend vs Getters

Both friend and public getters allow external code to read private data. They differ in scope:

friendPublic getter
Who can accessSpecific named functions/classesEveryone
Encapsulation impactLimited — known collaborators onlyLower — exposed to all
PerformanceDirect accessSame after inlining (trivial getters compile away)
Typical useOperators, tight collaboratorsGeneral API

Prefer public getters for data that any caller legitimately needs. Use friend when the collaboration is tight and you want to prevent the data from being accidentally accessed by everyone.

A useful way to decide: adding a getter changes the class’s public API for all users forever, while a friend is a private arrangement visible in the class definition. When I review code, the pattern I push back on is a class that grows a getter for every field just so one operator or one serializer can read them — that is a wider hole in encapsulation than a single friend declaration would be. The opposite smell is a friend class used to avoid thinking about an interface at all, where Engine reaches into five of Car’s fields; the tight coupling is then real, and the fix is usually to move that behavior into Car itself.


Over-friending, symmetry, inheritance, and template friends

Over-friending: declaring many classes as friends defeats encapsulation. Keep the friend list to a minimum — usually just operator<< and one or two specific collaborators.

Assuming friendship is symmetric: if A declares B as a friend, B can access A’s private members. But A cannot access B’s private members unless B also declares A as a friend.

Assuming friendship is inherited:

class Base {
    int secret_;
    friend void readSecret(const Base& b);  // only for Base
};

class Derived : public Base {
    int derived_secret_;
};

void readSecret(const Base& b) {
    b.secret_;  // OK
}

void readSecret(const Derived& d) {
    // d.derived_secret_;  // ERROR — not a friend of Derived
    // static_cast<const Base&>(d).secret_;  // ERROR too — this overload is a
    //                                       // different function, not Base's friend
    readSecret(static_cast<const Base&>(d));  // OK — delegate to the real friend
}

Friendship is granted to one exact function signature. The readSecret(const Derived&) overload shares a name with the friend but is a separate function, so it gets no access to Base::secret_ at all; it can only call the befriended overload. The same exactness applies when the declaration and definition drift apart: if the class declares friend void readSecret(const Base& b); but the definition takes Base& (non-const), the definition is a new overload, and the error message says secret_ “is private within this context” — which looks like the friend declaration was ignored.

Friendship is also not transitive: if A befriends B and B befriends C, C has no access to A.

Declaring a friend operator in a class template, and defining it outside:

template <typename T>
class Box {
    T value_;
public:
    explicit Box(T v) : value_(v) {}
    friend std::ostream& operator<<(std::ostream& os, const Box<T>& b);  // declares a NON-template
};

template <typename T>
std::ostream& operator<<(std::ostream& os, const Box<T>& b) { return os << b.value_; }

std::cout << Box<int>(3);   // links? no: undefined reference to operator<<(ostream&, const Box<int>&)

This is the friend mistake that produces the most confusing error. For each Box<T>, the friend declaration introduces an ordinary, non-template function operator<<(std::ostream&, const Box<int>&), which is a better match than the function template defined below it. Overload resolution picks the non-template, nobody ever defines it, and the program fails at link time. GCC does warn at the declaration, friend declaration ... declares a non-template function [-Wnon-template-friend], with a note suggesting to add <> after the function name, but the warning is easy to miss among others.

The simplest fix is the hidden-friend form from earlier: define the operator inside the class body, where Box<T> is known, and every instantiation gets its own inline non-template function. The alternative is to befriend the template explicitly, which needs a forward declaration of the template before the class and friend std::ostream& operator<< <>(std::ostream&, const Box<T>&); inside it. That version is more typing and grants friendship only to the matching specialization, but it keeps the definition out of the class.


When friend is the right tool

  • friend grants a specific function or class access to private and protected members
  • operator<< almost always needs to be a non-member friend (stream is on the left)
  • Symmetric binary operators (+, -, *) benefit from non-member friend status — both a+b and b+a work naturally
  • Friend classes grant access to all methods — appropriate for tightly coupled pairs (iterator/container, builder/product)
  • Factory functions with private constructors use friend to control object creation
  • Friendship is not inherited and not symmetric
  • Prefer public getters for general access; use friend for specific named collaborators with a tight coupling justification

Frequently Asked Questions (FAQ)

Q. Why is operator<< usually written as a friend instead of a member function?

A. A member operator always takes the class object as its left operand, so a member operator<< would have to be called as obj << std::cout, which is backwards. Writing it as a non-member lets std::ostream& be the left operand, and declaring it friend gives it access to the private fields it prints. If the class already exposes enough through public getters, a plain non-member function without friend works just as well.