Skip to content
C++
Idiom
since C++98
Basic

Virtual Friend Function

Simulate virtual behavior in non-member friend functions by delegating to a protected virtual member function.

Virtual Friend Functionsince C++98

A pattern that grants non-member friend functions polymorphic behavior by having them delegate to a protected or private virtual member function, working around the fact that friend functions cannot themselves be declared virtual.

Overview

Friend functions in C++ are non-member functions with privileged access to a class's private and protected members. Because they are non-member functions, they cannot be declared virtual β€” virtual dispatch requires a receiver object and a vtable slot, neither of which exist for free functions.

This creates a practical problem: non-member functions that are part of a class's interface β€” most notably operator<<, operator>>, and comparison operators β€” often need to behave polymorphically across a class hierarchy. A base class pointer or reference should dispatch to the correct derived-class logic, but there is no direct mechanism to achieve this with a free function.

The Virtual Friend Function idiom resolves this by splitting responsibility across two functions:

  1. A non-virtual friend in the base class, which provides the non-member interface and is found by argument-dependent lookup (ADL).
  2. A virtual (often pure virtual) protected member, which the friend delegates to and which derived classes override.

The friend function never changes. It is declared once in the base class and is not inherited β€” friend declarations are not subject to inheritance in C++. The virtual member is what travels down the hierarchy.

Syntax

cpp
class Base {
public:
    virtual ~Base() = default;                  // C++11: = default

    // Non-virtual friend: the public interface
    friend std::ostream& operator<<(std::ostream& os, const Base& obj) {
        return obj.print(os);                   // delegates to virtual
    }

protected:
    // Virtual hook: derived classes customise here
    virtual std::ostream& print(std::ostream& os) const = 0;
};

class Derived : public Base {
protected:
    std::ostream& print(std::ostream& os) const override { // C++11: override
        return os << "Derived{}";
    }
};

The print member is protected to prevent callers from bypassing the friend and invoking it directly. Marking it pure virtual (= 0) forces every concrete derived class to provide an implementation.

Examples

Polymorphic output streaming

The canonical use case is operator<<. Without this idiom, you would need to write a separate overload for every concrete type, or resort to dynamic_cast chains inside a single overload.

cpp
#include <iostream>
#include <memory>
#include <vector>

class Shape {
public:
    virtual ~Shape() = default;

    friend std::ostream& operator<<(std::ostream& os, const Shape& s) {
        return s.print(os);
    }

protected:
    virtual std::ostream& print(std::ostream& os) const = 0;
};

class Circle : public Shape {
public:
    explicit Circle(double radius) : radius_(radius) {}

protected:
    std::ostream& print(std::ostream& os) const override {
        return os << "Circle(r=" << radius_ << ")";
    }

private:
    double radius_;
};

class Rectangle : public Shape {
public:
    Rectangle(double w, double h) : w_(w), h_(h) {}

protected:
    std::ostream& print(std::ostream& os) const override {
        return os << "Rect(" << w_ << "x" << h_ << ")";
    }

private:
    double w_, h_;
};

int main() {
    // C++11: initializer list + unique_ptr; C++14: make_unique
    std::vector<std::unique_ptr<Shape>> shapes;
    shapes.push_back(std::make_unique<Circle>(5.0));
    shapes.push_back(std::make_unique<Rectangle>(3.0, 4.0));

    for (const auto& s : shapes) {   // C++11: range-for, auto
        std::cout << *s << '\n';     // dispatches correctly via vtable
    }
    // Output:
    // Circle(r=5)
    // Rect(3x4)
}

The single operator<< overload on Shape& handles the entire hierarchy. ADL ensures it is found whenever a Shape subtype is streamed.

Extending the pattern beyond output

The idiom is not limited to streaming. Any non-member operation that requires polymorphism can use it. Here, a serialisation framework needs a to_json() free function that behaves polymorphically:

cpp
#include <string>

class JsonSerializable {
public:
    virtual ~JsonSerializable() = default;

    friend std::string to_json(const JsonSerializable& obj) {
        return obj.serialize();
    }

protected:
    virtual std::string serialize() const = 0;
};

class Point : public JsonSerializable {
public:
    Point(int x, int y) : x_(x), y_(y) {}

protected:
    std::string serialize() const override {
        return "{\"x\":" + std::to_string(x_) +
               ",\"y\":" + std::to_string(y_) + "}";
    }

private:
    int x_, y_;
};

class NamedPoint : public Point {
public:
    NamedPoint(int x, int y, std::string name)
        : Point(x, y), name_(std::move(name)) {}  // C++11: move

protected:
    std::string serialize() const override {
        // Call parent's virtual, then augment
        std::string base = Point::serialize();
        base.pop_back();                         // strip closing '}'
        return base + ",\"name\":\"" + name_ + "\"}";
    }

private:
    std::string name_;
};

to_json() is a free function usable in generic code and found by ADL, yet it dispatches to the right derived implementation.

With C++20 concepts for type-safe constraints

Since C++20, you can constrain template parameters using concepts to ensure a type participates in this protocol:

cpp
#include <concepts>
#include <ostream>

template <typename T>
concept Printable = requires(const T& t, std::ostream& os) {
    { t.print(os) } -> std::same_as<std::ostream&>;   // C++20: concept + requires
};

// Generic algorithm only accepts types with a print() member
template <Printable T>
void log(std::ostream& os, const T& obj) {
    obj.print(os) << '\n';
}

Note that exposing print as public to satisfy a concept trades the encapsulation advantage of the idiom for generic interoperability. Whether this is acceptable depends on your API contract.

Best Practices

Make the virtual hook protected, not public. The friend function is the intended public interface. Exposing the virtual member publicly creates a second, unsanctioned entry point that bypasses any pre/post-processing in the friend.

Declare the hook pure virtual in abstract bases. = 0 ensures every concrete class provides a definition and prevents accidentally inheriting a do-nothing default.

Return std::ostream& from print, not void. This allows the friend to return obj.print(os) in one expression, avoiding a silent mismatch where the friend returns os but print already flushed or moved it.

Name the private virtual consistently across your hierarchy. print, serialize, to_stream β€” pick one per concern and stick to it. Inconsistent naming makes it harder to audit which virtual hook a given friend delegates to.

Be mindful of friend non-inheritance. If a derived class wants to provide its own non-member overload for some reason, it must do so explicitly. There is no mechanism for a derived class to "inherit" the friend declaration.

Common Pitfalls

Object slicing through the friend. The friend must accept const Base&, not const Base by value. Passing by value slices the object, discards the derived portion, and the virtual call dispatches to the base's (possibly pure) print.

cpp
// WRONG: slices the object, virtual dispatch is broken
friend std::ostream& operator<<(std::ostream& os, const Shape s) {
    return s.print(os);  // always calls Shape::print, which may be pure
}

// CORRECT: reference preserves the dynamic type
friend std::ostream& operator<<(std::ostream& os, const Shape& s) {
    return s.print(os);
}

Forgetting that friend declarations are not inherited. New developers sometimes expect Derived to automatically "have" the same operator<< as Base via normal inheritance rules. In reality, the friend is re-found by ADL each time via the Base class, which works correctly as long as parameters are taken by Base&. If a function takes Derived& specifically, it needs its own friend declaration or the base's overload won't match.

Calling the virtual hook from the constructor or destructor. This is a general virtual-dispatch hazard, not unique to this idiom, but it surfaces here when print is called during construction (e.g., in a logging base constructor). During construction of Base, the dynamic type is Base, so the pure virtual print is invoked β€” undefined behaviour.

Exposing the hook as public to satisfy a concept or mixin. Doing so undermines the encapsulation rationale of the idiom. If external callers need direct access to print, reconsider whether the friend function is the right interface at all.

Polymorphic Comparison with operator== and operator<=> (C++20)

Direct operator== between base-class references has the same fundamental problem as operator<<: the function is non-member, cannot be virtual, yet must dispatch polymorphically. The fix follows the same delegation pattern β€” a non-virtual friend calls a protected virtual comparator.

cpp
// C++11
#include <typeinfo>

class Shape {
public:
    virtual ~Shape() = default;

    friend bool operator==(const Shape& a, const Shape& b) {
        return a.equals(b);                        // delegate to virtual hook
    }
    friend bool operator!=(const Shape& a, const Shape& b) {
        return !(a == b);
    }

protected:
    virtual bool equals(const Shape& other) const = 0;
};

class Circle : public Shape {
public:
    explicit Circle(double r) : r_(r) {}

protected:
    bool equals(const Shape& other) const override {
        // dynamic type mismatch β†’ never equal
        const auto* rhs = dynamic_cast<const Circle*>(&other);
        if (!rhs) return false;
        return r_ == rhs->r_;
    }

private:
    double r_;
};

class Rectangle : public Shape {
public:
    Rectangle(double w, double h) : w_(w), h_(h) {}

protected:
    bool equals(const Shape& other) const override {
        const auto* rhs = dynamic_cast<const Rectangle*>(&other);
        if (!rhs) return false;
        return w_ == rhs->w_ && h_ == rhs->h_;
    }

private:
    double w_, h_;
};

Circle == Rectangle falls through the dynamic_cast guard and returns false without undefined behaviour. Circle == Circle compares radii through the vtable.

In C++20, you can extend this to three-way comparison. Use std::partial_ordering rather than std::strong_ordering because heterogeneous pairs (e.g., Circle vs Rectangle) are genuinely incomparable β€” they are neither less than, equal to, nor greater than each other in any meaningful sense.

cpp
// C++20
#include <compare>

class Shape {
public:
    virtual ~Shape() = default;

    friend std::partial_ordering operator<=>(const Shape& a, const Shape& b) {
        return a.compare(b);
    }
    friend bool operator==(const Shape& a, const Shape& b) {
        return a.compare(b) == std::partial_ordering::equivalent;
    }

protected:
    virtual std::partial_ordering compare(const Shape& other) const = 0;
};

class Circle : public Shape {
public:
    explicit Circle(double r) : r_(r) {}

protected:
    std::partial_ordering compare(const Shape& other) const override {
        const auto* rhs = dynamic_cast<const Circle*>(&other);
        if (!rhs) return std::partial_ordering::unordered;  // incomparable types
        return r_ <=> rhs->r_;                              // C++20: <=>
    }

private:
    double r_;
};

Prefer partial_ordering over weak_ordering or strong_ordering in polymorphic hierarchies. Heterogeneous comparisons must be able to signal "unordered" β€” only partial_ordering provides that sentinel value.

std::formatter Specialization for Polymorphic Types (C++20)

std::format("{}", shape) requires a std::formatter<Shape> specialization, but Shape is abstract and its concrete type is not known at the point of specialization. The solution mirrors the friend-delegation pattern: specialize std::formatter<Shape> to call a protected virtual hook on the object.

cpp
// C++20
#include <format>
#include <string_view>

class Shape {
public:
    virtual ~Shape() = default;

    // Non-member streaming friend unchanged
    friend std::ostream& operator<<(std::ostream& os, const Shape& s) {
        return s.print(os);
    }

protected:
    virtual std::ostream& print(std::ostream& os) const = 0;

    // New hook for std::format support
    virtual void format_to(std::format_context::iterator& out) const = 0;

    // Grant formatter access to the protected hook
    friend struct std::formatter<Shape>;
};

template <>
struct std::formatter<Shape> {
    constexpr auto parse(std::format_parse_context& ctx) { return ctx.begin(); }

    auto format(const Shape& s, std::format_context& ctx) const {
        auto out = ctx.out();
        s.format_to(out);          // virtual dispatch through the friend grant
        return out;
    }
};

class Circle : public Shape {
public:
    explicit Circle(double r) : r_(r) {}

protected:
    std::ostream& print(std::ostream& os) const override {
        return os << "Circle(r=" << r_ << ")";
    }

    void format_to(std::format_context::iterator& out) const override {
        std::format_to(out, "Circle(r={})", r_);   // C++20: format_to
    }

private:
    double r_;
};

With this specialization in place, std::print("{}\n", *shape_ptr) works for any concrete Shape β€” including those defined in other translation units β€” without any additional overloads.

Declare friend struct std::formatter<Shape> in the base class. Because format_to is protected, the formatter specialization cannot reach it without the explicit friend grant. This is intentional: it keeps format_to off the public API while still permitting standard-library infrastructure to call it.

CRTP Alternative: No Runtime Dispatch

The Curiously Recurring Template Pattern (CRTP) achieves the same "customization via derived class" goal without a vtable. The dispatch is resolved at compile time through a static_cast.

cpp
// C++11
#include <iostream>

template <typename Derived>
class PrintableBase {
public:
    // Friend operator in CRTP base β€” D is the concrete type
    friend std::ostream& operator<<(std::ostream& os, const PrintableBase<Derived>& b) {
        // Static cast: safe because Derived inherits from PrintableBase<Derived>
        return static_cast<const Derived&>(b).do_print(os);
    }

protected:
    // No virtual keyword β€” resolved at compile time
    std::ostream& do_print(std::ostream& os) const {
        return os << "[PrintableBase]";    // optional default
    }
};

class Circle : public PrintableBase<Circle> {
public:
    explicit Circle(double r) : r_(r) {}

    // Not virtual β€” called directly by static_cast in the base
    std::ostream& do_print(std::ostream& os) const {
        return os << "Circle(r=" << r_ << ")";
    }

private:
    double r_;
};

class Rectangle : public PrintableBase<Rectangle> {
public:
    Rectangle(double w, double h) : w_(w), h_(h) {}

    std::ostream& do_print(std::ostream& os) const {
        return os << "Rect(" << w_ << "x" << h_ << ")";
    }

private:
    double w_, h_;
};

int main() {
    Circle c(5.0);
    Rectangle r(3.0, 4.0);
    std::cout << c << '\n';   // Circle(r=5)
    std::cout << r << '\n';   // Rect(3x4)
}

The key difference: CRTP dispatch is compile-time (static polymorphism), incurring approximately zero nanoseconds of runtime overhead β€” no vtable lookup, no indirect branch, no pointer indirection.

Use CRTP when all concrete types are known at compile time. The moment you need to store heterogeneous types in a std::vector<Base*> or accept a Base& parameter whose dynamic type is unknown, CRTP breaks down β€” each instantiation of PrintableBase<T> is an unrelated type. Use the virtual-friend idiom whenever runtime polymorphism through a common base pointer is required.

Inheritance Chain with Default Implementation

Abstract bases, intermediate classes with defaults, and fully-specified leaf classes can all coexist in the same hierarchy. The friend is declared once in the root base and never needs to change.

cpp
// C++11
#include <iostream>

// Root: pure virtual β€” no default
class Base {
public:
    virtual ~Base() = default;

    friend std::ostream& operator<<(std::ostream& os, const Base& obj) {
        return obj.print(os);
    }

protected:
    virtual std::ostream& print(std::ostream& os) const = 0;
};

// Intermediate: provides a sensible default; not abstract
class Intermediate : public Base {
protected:
    std::ostream& print(std::ostream& os) const override {
        return os << "Intermediate{}";
    }
};

// Leaf: opts in to a richer format, calls parent explicitly
class Detailed : public Intermediate {
public:
    explicit Detailed(int id) : id_(id) {}

protected:
    std::ostream& print(std::ostream& os) const override {
        os << "Detailed[";
        Intermediate::print(os);    // explicit non-virtual call to parent
        return os << ", id=" << id_ << "]";
    }

private:
    int id_;
};

int main() {
    Detailed d(42);
    Base& b = d;
    std::cout << b << '\n';   // Detailed[Intermediate{}, id=42]
}

Intermediate::print(os) inside Detailed::print is a direct, non-virtual call to the parent's implementation β€” standard C++ qualified-name syntax. This allows incremental output building without duplicating the parent's logic.

The friend is still declared only in Base and handles the entire chain without modification. Any future leaf class appended to the hierarchy automatically gains operator<< through ADL, provided it inherits from Base.

Thread Safety of the Delegation Pattern

The friend function itself is stateless β€” it holds no data and takes no locks. Thread safety is the responsibility of the virtual hook implementation in each derived class.

If print() reads from fields that are simultaneously mutated by another thread, the derived override must protect those reads. The friend cannot do this because it does not know the derived type or which mutex governs its data.

cpp
// C++11
#include <mutex>
#include <ostream>
#include <string>

class LiveStatus : public Base {
public:
    void update(std::string msg) {
        std::lock_guard<std::mutex> lock(mutex_);
        message_ = std::move(msg);
    }

protected:
    std::ostream& print(std::ostream& os) const override {
        // Lock inside the virtual hook β€” the only place that knows the mutex
        std::lock_guard<std::mutex> lock(mutex_);
        return os << "Status(" << message_ << ")";
    }

private:
    mutable std::mutex mutex_;   // mutable: locked inside const member
    std::string message_;
};

mutable std::mutex is required because print is const β€” the logical constness of the object (its observable value) is preserved even though the mutex is physically modified during lock acquisition.

The virtual hook must acquire its own lock; the friend cannot. Any attempt to "move the lock up" into the friend function would require the friend to know the derived type, defeating the entire purpose of the idiom. If you need a coarser lock that spans multiple operations, wrap those operations at the call site rather than inside the hook.

Alternative: virtual std::string to_string() + Free str() Function

Instead of delegating through a streaming hook, some designs prefer a virtual method that returns a std::string. A free function found by ADL then wraps it.

cpp
// C++11
#include <string>

class Base {
public:
    virtual ~Base() = default;

    // ADL-visible free function as the public interface
    friend std::string str(const Base& b) {
        return b.to_string();
    }

protected:
    virtual std::string to_string() const = 0;
};

class Circle : public Base {
public:
    explicit Circle(double r) : r_(r) {}

protected:
    std::string to_string() const override {
        return "Circle(r=" + std::to_string(r_) + ")";
    }

private:
    double r_;
};

The returned std::string is fully composable: it can be logged to any sink, stored in a container, compared with ==, or passed to a test assertion without threading an ostream through the call chain.

The trade-off is allocation cost. Every call to str() heap-allocates a new std::string, regardless of whether the caller intends to immediately stream it or discard it. For high-frequency output (e.g., logging millions of events per second), this allocation pressure is measurable.

In C++23, std::format_to into a pre-allocated buffer or a std::back_insert_iterator<std::string> bridges both patterns: you get the composability of a string with the allocation control of a streaming approach.

cpp
// C++23
#include <format>
#include <string>

class Base {
public:
    virtual ~Base() = default;

protected:
    virtual void format_to_string(std::string& out) const = 0;

    friend std::string str(const Base& b) {
        std::string result;
        b.format_to_string(result);   // append into caller-owned buffer
        return result;
    }
};

Decision guide β€” streaming pattern vs. to_string pattern:

CriterionStreaming (print hook)to_string hook
Heap allocationNone (writes directly to stream)One std::string per call
Output flexibilityostream, file, networkAny sink after construction
TestabilityRequires capturing ostringstreamDirect string comparison
Large outputsEfficient (incremental write)Loads entire output into memory
C++20 std::formatVia std::formatter specializationStraightforward

For console and file output where throughput matters, prefer the streaming delegation pattern. For test-heavy code where direct string comparison is valuable, the to_string variant is simpler to work with.

See Also