Cross-Class Access with Friend Declarations
Allow one class to access the private members of another class.
What Are Friend Classes and Friend Member Functions?
A class can hand its private and protected members to an outsider in three different sizes. The previous lesson covered the smallest one, a single non-member function. This lesson covers the other two: a friend class, whose every member function may reach inside, and a friend member function, where exactly one member function of some other class is let in.
The keyword and the direction of the permission never change. The grant is always written inside the class whose members are being exposed, and it always names the beneficiary. What changes between the three forms is how much of the outside world the single line covers, and, as it turns out, how much the compiler needs to already know at the point where you write it.
Three Sizes of Grant
Written inside class Reservoir |
Who gets access to Reservoir's privates |
What the compiler must already know |
|---|---|---|
friend void printLevel(const Reservoir&); |
that one free function | nothing, the declaration introduces the function |
friend void Inspector::record(const Reservoir&); |
that one member of Inspector |
the complete definition of Inspector |
friend class Inspector; |
every member function of Inspector, present and future |
nothing, the declaration introduces the class |
The middle row is the awkward one, and the third column is why. Everything in this lesson follows from that table, so it is worth coming back to once you have read the rest.
Pick the narrowest row that does the job. A friend class is a standing invitation: any member function anyone adds to Inspector next year arrives already holding the key, and nobody has to touch Reservoir to make that happen.
Naming a Whole Class
An irrigation controller reads a soil probe. The probe keeps its raw sensor count and the threshold it was calibrated against private, because those are meaningless outside a decision about watering, and the controller is the only thing that makes that decision.
#include <iostream>
class SoilProbe
{
private:
int m_rawMoisture{};
int m_dryThreshold{};
public:
SoilProbe(int rawMoisture, int dryThreshold)
: m_rawMoisture{ rawMoisture }, m_dryThreshold{ dryThreshold }
{
}
// Every member function of IrrigationTimer may reach inside a SoilProbe
friend class IrrigationTimer;
};
class IrrigationTimer
{
private:
int m_minutesPerCycle{};
public:
explicit IrrigationTimer(int minutesPerCycle)
: m_minutesPerCycle{ minutesPerCycle }
{
}
void schedule(const SoilProbe& probe) const
{
if (probe.m_rawMoisture < probe.m_dryThreshold)
std::cout << "Bed reads " << probe.m_rawMoisture << ", watering " << m_minutesPerCycle << " minutes" << '\n';
else
std::cout << "Bed reads " << probe.m_rawMoisture << ", no cycle needed" << '\n';
}
};
int main()
{
const SoilProbe bedOne{ 412, 500 };
const SoilProbe bedTwo{ 663, 500 };
const IrrigationTimer timer{ 12 };
timer.schedule(bedOne);
timer.schedule(bedTwo);
return 0;
}
Output:
Bed reads 412, watering 12 minutes
Bed reads 663, no cycle needed
Three things in that program are worth pinning down.
The permission is one line, and it lives in SoilProbe. IrrigationTimer cannot write it, mention it, or opt into it. A class is never able to declare itself a friend of another.
The permission covers IrrigationTimer as a whole. schedule() is the only member that uses it today, but a logReading() added tomorrow would inherit the same reach with no further edit to SoilProbe.
The permission says nothing about which probe. schedule() reads bedOne and bedTwo in turn, and would read any other SoilProbe handed to it. Friendship is granted per class, not per object.
The Grant Also Declares the Class
Notice that IrrigationTimer was never forward declared above SoilProbe, even though SoilProbe names it. It did not need to be. When a friend declaration mentions a class the compiler has not seen, the declaration itself introduces that class name into the surrounding namespace, so friend class IrrigationTimer; is doing two jobs at once: declaring that such a class exists, and granting it access.
That introduced name is not yet usable elsewhere. Until the class is declared or defined normally, ordinary name lookup will not find it, so a line like
IrrigationTimer* timer{}; written after SoilProbe but before class IrrigationTimer is still rejected, with GCC reporting that IrrigationTimer does not name a type. The friend declaration saves you from needing a forward declaration for the grant, not for everything else you might want to write.
What Friendship Does Not Give You
Friendship is a single arrow between two named parties, and almost every mistake people make with it comes from expecting the arrow to spread.
| The hope | The reality |
|---|---|
A befriends B, so B befriends A |
No. Friendship is not reciprocal. Each direction is a separate grant. |
A befriends B, B befriends C, so A sees C |
No. Friendship is not transitive. |
A befriends B, so classes derived from B are also friends |
No. Friendship is not inherited. |
| A friend class gets the other class's objects for free | No. There is no implicit object, so the object has to arrive as a parameter. |
The last row is the one that surprises people who think of friendship as a kind of merger. A member function of IrrigationTimer has a this pointer to its own IrrigationTimer, and no pointer of any sort to a SoilProbe. That is why schedule() takes const SoilProbe&, and why a SoilProbe reference is the only thing that makes the private members reachable at all.
The first row is easy to demonstrate. Here SoilProbe prints a summary that mentions the timer's cycle length, which is private to IrrigationTimer. The grant in SoilProbe runs the other way, so this example is broken on purpose:
#include <iostream>
class IrrigationTimer;
class SoilProbe
{
private:
int m_rawMoisture{};
public:
explicit SoilProbe(int rawMoisture) : m_rawMoisture{ rawMoisture } {}
void describeCycle(const IrrigationTimer& timer) const;
friend class IrrigationTimer;
};
class IrrigationTimer
{
private:
int m_minutesPerCycle{};
public:
explicit IrrigationTimer(int minutesPerCycle) : m_minutesPerCycle{ minutesPerCycle } {}
};
void SoilProbe::describeCycle(const IrrigationTimer& timer) const
{
std::cout << m_rawMoisture << " on a " << timer.m_minutesPerCycle << " minute cycle" << '\n';
}
int main()
{
const SoilProbe bedOne{ 412 };
const IrrigationTimer timer{ 12 };
bedOne.describeCycle(timer);
return 0;
}
s.cpp: In member function 'void SoilProbe::describeCycle(const IrrigationTimer&) const':
s.cpp:29:53: error: 'int IrrigationTimer::m_minutesPerCycle' is private within this context
29 | std::cout << m_rawMoisture << " on a " << timer.m_minutesPerCycle << " minute cycle" << '\n';
| ^~~~~~~~~~~~~~~~~
s.cpp:21:9: note: declared private here
21 | int m_minutesPerCycle{};
| ^~~~~~~~~~~~~~~~~
Two classes can be friends of each other, but only by saying so twice, once in each body:
#include <iostream>
class IrrigationTimer;
class SoilProbe
{
private:
int m_rawMoisture{};
public:
explicit SoilProbe(int rawMoisture) : m_rawMoisture{ rawMoisture } {}
void describeCycle(const IrrigationTimer& timer) const;
friend class IrrigationTimer; // SoilProbe opens itself to IrrigationTimer
};
class IrrigationTimer
{
private:
int m_minutesPerCycle{};
public:
explicit IrrigationTimer(int minutesPerCycle) : m_minutesPerCycle{ minutesPerCycle } {}
friend class SoilProbe; // and IrrigationTimer opens itself to SoilProbe
};
void SoilProbe::describeCycle(const IrrigationTimer& timer) const
{
std::cout << m_rawMoisture << " on a " << timer.m_minutesPerCycle << " minute cycle" << '\n';
}
int main()
{
const SoilProbe bedOne{ 412 };
const IrrigationTimer timer{ 12 };
bedOne.describeCycle(timer);
return 0;
}
Output:
412 on a 12 minute cycle
Mutual friendship is worth a second look before you write it, because it means two classes are now free to depend on each other's representation in both directions. Often the honest reading of that situation is that the two classes are really one.
Narrowing the Grant to One Member Function
If only one function of the other class actually needs to look inside, name that function instead of the whole class. The syntax is the non-member form with the class name attached:
friend void WaterBudget::deduct(const FlowMeter& meter);
This is where the third column of the opening table starts to bite. To check that deduct really is a member of WaterBudget, and that its parameters match, the compiler has to look inside WaterBudget, which means it needs the complete definition of that class, not merely a promise that the name exists.
A forward declaration is not enough, so this example is broken on purpose:
#include <iostream>
class WaterBudget; // a forward declaration, nothing more
class FlowMeter
{
private:
int m_litresDelivered{};
public:
explicit FlowMeter(int litresDelivered) : m_litresDelivered{ litresDelivered } {}
friend void WaterBudget::deduct(const FlowMeter& meter);
};
class WaterBudget
{
private:
int m_litresRemaining{};
public:
explicit WaterBudget(int litresRemaining) : m_litresRemaining{ litresRemaining } {}
void deduct(const FlowMeter& meter) { m_litresRemaining -= meter.m_litresDelivered; }
void report() const { std::cout << m_litresRemaining << " litres left this week" << '\n'; }
};
int main()
{
WaterBudget weekly{ 2400 };
const FlowMeter morningRun{ 365 };
weekly.deduct(morningRun);
weekly.report();
return 0;
}
s.cpp:13:59: error: invalid use of incomplete type 'class WaterBudget'
13 | friend void WaterBudget::deduct(const FlowMeter& meter);
| ^
s.cpp:3:7: note: forward declaration of 'class WaterBudget'
3 | class WaterBudget; // a forward declaration, nothing more
| ^~~~~~~~~~~
s.cpp: In member function 'void WaterBudget::deduct(const FlowMeter&)':
s.cpp:24:70: error: 'int FlowMeter::m_litresDelivered' is private within this context
24 | void deduct(const FlowMeter& meter) { m_litresRemaining -= meter.m_litresDelivered; }
| ^~~~~~~~~~~~~~~~~
The second error is the first one's shadow. Since the friend declaration was rejected, no grant was ever made, and deduct() is left poking at a private member with no permission.
Befriending a whole class needs nothing but the class name. Befriending one member function needs the complete definition of the class that owns it. Mixing those two rules up is the single most common reason a
friend line fails to compile.
The Layout That Satisfies Both Sides
Fixing the program means arranging four things so that each one only asks for what has already been seen. Each row below can only be written once the row above it is in place:
| Order | What you write | Why it can go no earlier |
|---|---|---|
| 1 | class FlowMeter; |
WaterBudget mentions the name, and a reference parameter needs nothing more than the name |
| 2 | the full WaterBudget definition, with deduct() declared but not defined |
its body would read FlowMeter's members, which are not visible yet |
| 3 | the full FlowMeter definition, containing the friend declaration |
naming WaterBudget::deduct requires the complete WaterBudget from step 2 |
| 4 | the definition of WaterBudget::deduct() |
reading m_litresDelivered requires the complete FlowMeter from step 3 |
Written out, the dependency chain compiles:
#include <iostream>
class FlowMeter; // 1. the name FlowMeter has to exist before WaterBudget mentions it
class WaterBudget
{
private:
int m_litresRemaining{};
public:
explicit WaterBudget(int litresRemaining) : m_litresRemaining{ litresRemaining } {}
void deduct(const FlowMeter& meter); // 2. declared only, since FlowMeter is still incomplete
void report() const { std::cout << m_litresRemaining << " litres left this week" << '\n'; }
};
class FlowMeter
{
private:
int m_litresDelivered{};
public:
explicit FlowMeter(int litresDelivered) : m_litresDelivered{ litresDelivered } {}
// 3. WaterBudget is complete here, so naming one of its members is legal
friend void WaterBudget::deduct(const FlowMeter& meter);
};
// 4. FlowMeter is complete here, so its private member can be read
void WaterBudget::deduct(const FlowMeter& meter)
{
m_litresRemaining -= meter.m_litresDelivered;
}
int main()
{
WaterBudget weekly{ 2400 };
const FlowMeter morningRun{ 365 };
weekly.report();
weekly.deduct(morningRun);
weekly.report();
return 0;
}
Output:
2400 litres left this week
2035 litres left this week
Two rules generate that whole arrangement, and they are worth memorising on their own, because they apply far beyond friendship:
- A reference or pointer to a class needs only a declaration of the class name.
- Touching anything inside a class, whether a data member or a member function name, needs the complete definition.
Why Separate Files Make This Disappear
None of that shuffling is inherent to friend member functions. It is the price of writing two mutually dependent classes into one file, top to bottom, with a compiler that reads in that order.
Split them the way the earlier lesson on header files described, and the arrangement falls out for free. WaterBudget.h holds its class, FlowMeter.h includes WaterBudget.h because its friend declaration needs the complete definition, and WaterBudget.cpp includes both headers before defining deduct(). Every .cpp file ends up with all the class definitions it needs, in an order the includes take care of, and nobody has to hand-sequence four fragments in a single translation unit.
Reach for a friend class when a whole helper class is genuinely part of another class's implementation. Reach for a friend member function when exactly one function needs the access and you want the grant to say so. If the ordering dance starts to dominate the file, that is a sign the two classes want separate headers, not a sign that friendship was the wrong tool.
Summary
| Point | What to remember |
|---|---|
| Friend class | friend class X; inside a class grants every member function of X access to that class's private and protected members |
| Who writes it | Always the class being exposed. Nothing outside a class can grant itself access to it |
| Per class, not per object | A friend class can reach into any object of the granting class it can get hold of |
| No implicit object | Friendship supplies no this pointer to the other class, so the object still has to be passed as a parameter |
| Not reciprocal | A befriending B says nothing about B befriending A. Mutual access takes two declarations |
| Not transitive | Friend of a friend is not a friend |
| Not inherited | Classes derived from a friend class do not inherit the friendship |
| Friend member function | friend void X::f(const Y&); grants access to that one member of X and nothing else |
| What each form needs | Befriending a class needs only its name. Befriending a member function needs the complete definition of its class |
| Declaration doubles up | A friend declaration naming an unseen class introduces that class name, so no separate forward declaration is required for the grant itself |
| Ordering rule | References and pointers need a declared name. Reaching inside needs a complete definition |
| In practice | Separate headers make the ordering problem vanish, because each translation unit ends up with the full definitions already included |
Friend classes buy simplicity, friend member functions buy precision, and both are grants a class makes deliberately about its own internals. Use the narrowest one that does the job, and let the include structure of a real project handle the ordering rather than fighting it inside one file.
How did you find this lesson?
Your rating helps us improve the content.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Cross-Class Access with Friend Declarations - Quiz
Test your understanding of the lesson.
Practice Exercises
Secure Vault with Friend Access
Create a Vault class with private data and a friend Auditor class that can access the private members. Demonstrate friend class relationships.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!