1. Motivation: The Problem with friend
In C++, friend is a double-edged sword. If you allow your friend to touch you inappropriately, that would be hurt. If class A befriends another class B, then everything in A becomes visible to B. friend grants all-or-nothing access — which is rarely what you actually want. You usually want your friend to only able to touch a portion of you.
Consider the following scenario. You have a Widget class that must be created through a factory, because the constructor needs to perform some privileged setup (registering with a resource manager, validating invariants, logging, and so on). But the constructor itself has to be public for std::make_unique or for the factory to reach it. The moment it is public, anyone can construct a Widget — and that is a correctness bug waiting to happen.
You might reach for friend:
class WidgetFactory;
class Widget {
friend class WidgetFactory; // grants access to EVERYTHING
public:
Widget(int x) : x_{x} {}
private:
int x_{};
};
class WidgetFactory {
public:
static std::unique_ptr<Widget> create(int x) {
return std::make_unique<Widget>(x);
}
};
int main()
{
auto w = WidgetFactory::create(42); // ok
// auto bad = std::make_unique<Widget>(42); // also ok! constructor is public, but you dont want this...
}
This "works," but it is a blunt instrument. WidgetFactory now has access to every private member of Widget, not just the constructor. As Widget grows, so does the accidental attack surface. And if you later need to grant the same narrow access to a Debugger class, you add another friend, and the surface widens again.
The question this idiom answers is:
How can I let a specific class touch only a specific portion of my class — and nothing else?
And then now let me introduce the PassKey Idiom.
2. The Classic Solution: A Nested PassKey
The core idea is to make a public function that requires an argument nobody else can construct. That argument is the passkey.
class Widget {
public:
class PassKey {
friend class Widget;
PassKey() = default; // this constructor is private
};
Widget(PassKey, int x)
: x_{x}
{}
static std::unique_ptr<Widget> create(int x)
{
return std::make_unique<Widget>(PassKey{}, x);
}
private:
int x_{};
};
int main()
{
// auto w1 = Widget{Widget::PassKey{}, 42}; // compile error! cannot access private constructor
auto w2 = Widget::create(42); // ok
}
A few things are worth calling out:
- The constructor
PassKey() = default;is implicitly private (it is a member of a class whose default access is private), andWidgetis its only friend. -
Widget's constructor ispublic, so it satisfies the syntactic requirements ofstd::make_unique. But it is only callable by code that can produce aPassKey— which, in practice, means onlyWidgetitself (or whateverWidgetchooses to befriend). - In
main, the direct construction fails to compile becauseWidget::PassKeyis private. The only way in is throughcreate.
This is a clean, zero-boilerplate solution for a single class. But it does not scale. If you have Widget, Gadget, and Sprocket, you must replicate this nested-class boilerplate in every one of them. Worse, the authorized "friend" is baked into the nested definition, so if you want Widget's constructor callable by both a Factory and a Debugger, you either befriend both inside PassKey or duplicate the pattern. How to solve this? we can use a template!
3. Scaling Up: The Templated PassKey
The templated PassKey decouples the mechanism from the class it protects. You define it once, and any class can use it:
template <typename T>
class Passkey {
friend T; // only T can construct a Passkey<T>
Passkey() = default; // private: no one else can create one
};
Now the authorized class can create a passkey, and unauthorized code cannot:
class WidgetFactory;
class WidgetDebugger;
class Widget {
public:
// Public constructor, but only callable by whoever can make a Passkey<WidgetFactory>
Widget(Passkey<WidgetFactory>, int x)
: x_{x}
{}
// A separate, narrower door for the debugger
void dump(Passkey<WidgetDebugger>) const { /* ... */ }
private:
int x_{};
};
class WidgetFactory {
public:
static std::unique_ptr<Widget> create(int x) {
return std::make_unique<Widget>(Passkey<WidgetFactory>{}, x);
}
};
class WidgetDebugger {
public:
void inspect(const Widget& w) {
w.dump(Passkey<WidgetDebugger>{});
}
};
int main()
{
auto w = WidgetFactory::create(42); // ok
WidgetDebugger{}.inspect(*w); // ok
// auto bad = std::make_unique<Widget>(Passkey<WidgetFactory>{}, 1); // error
// WidgetDebugger{}.inspect(...) can't call Widget's ctor; Factory can't call dump()
}
The design now has two key properties:
-
Per-function granularity.
Widget's constructor admits onlyWidgetFactory.Widget::dumpadmits onlyWidgetDebugger. Neither can use the other's door. Nofriendgrants blanket access. -
Reusable mechanism.
Passkey<T>is written once and works for any number of classes. There is no nested-class boilerplate to copy.
A subtle but important point: std::make_unique cannot construct the Passkey for you. The authorized class must explicitly pass Passkey<WidgetFactory>{} into the constructor call. That is by design — make_unique is not the privileged party; the caller is.
Improvement you can made if you are using C++20
C++20 does not fundamentally change the PassKey mechanism. There is no concept that replaces it, because concepts constrain types, whereas PassKey constrains who can produce a token. The two compose nicely, though: if you are building a generic system where the set of authorized callers is itself parameterized, you can write
#include <type_traits>
// Marker: only types deriving from this may be a Caller.
struct Privileged {};
// Passkey<T> only exists if T is privileged.
template <typename T>
requires std::is_base_of_v<Privileged, T>
class Passkey {
friend T;
Passkey() = default;
};
class Widget {
public:
Widget(Passkey<class Factory>, int x) : x_{x} {}
private:
int x_{};
};
// Factory opts in — so it can make a Passkey<Factory>.
class Factory : Privileged {
public:
static Widget create(int x) { return Widget(Passkey<Factory>{}, x); }
};
// Sneaky does NOT opt in — so Passkey<Sneaky> won't compile.
class Sneaky {
public:
// Widget w{Passkey<Sneaky>{}, 1}; // error: constraint not satisfied
};
That's the whole idea. Widget's constructor is public, but the only way to call it is to hand it a Passkey<Factory> — and only Factory can construct one, because Factory is the friend. The requires clause adds one extra guarantee: you can't even write Passkey<X> unless X derives from Privileged, so unauthorized types fail at the template level instead of at the friend level.
4. Group Permissions: Variadic Friends (C++26)
The templated PassKey has one remaining friction point. Suppose a single function should be callable by several authorized classes — say a Factory, a Debugger, and a Serializer. Before C++26, you cannot write friend Ts...; expanding a parameter pack in a friend declaration is ill-formed. The workarounds are ugly: manual overloads, a combined passkey type, or a chain of nested friends.
C++26 fixes this with variadic friends (P2893R3). You can now expand a pack directly:
#include <iostream>
// C++26: variadic friends — one passkey authorizes a whole pack of types.
template <typename... Ts>
class Passkey {
friend Ts...;
Passkey() = default;
};
class Factory;
class Debugger;
class Serializer;
class Widget {
public:
// Only Factory, Debugger, and Serializer can call this.
void secretInit(Passkey<Factory, Debugger, Serializer>) {
std::cout << "Widget initialized\n";
}
};
// Each authorized type can build the passkey — and thus call secretInit.
class Factory {
public:
void make(Widget& w) { w.secretInit(Passkey<Factory, Debugger, Serializer>{}); }
};
class Debugger {
public:
void poke(Widget& w) { w.secretInit(Passkey<Factory, Debugger, Serializer>{}); }
};
class Serializer {
public:
void save(Widget& w) { w.secretInit(Passkey<Factory, Debugger, Serializer>{}); }
};
// Not in the pack — cannot construct the passkey.
class Intruder {
public:
// void tryIt(Widget& w) { w.secretInit(Passkey<Factory, Debugger, Serializer>{}); }
// ^ error: Passkey ctor is private; Intruder is not a friend.
};
int main()
{
Widget w;
Factory{}.make(w); // ok
Debugger{}.poke(w); // ok
Serializer{}.save(w); // ok
}
This makes the "group permission" case a one-liner and removes the need for the combined-type hack. It is a small language feature, but for the PassKey idiom it is a meaningful ergonomic win: the passkey now expresses "these N types, and no others" directly in the type system.
5. Performance and Trade-offs
Runtime cost
In theory, passing an empty object by value could cost a register or a stack slot. In practice, in optimized builds, it is a zero-cost abstraction. Because Passkey has no data members and the parameter is unnamed, the optimizer elides it entirely at -O1 and above. There is no runtime check, no branch, no token comparison — the entire mechanism lives in the type system and disappears in the emitted code.
The one caveat
If the protected function is not inlined (defined in a separate translation unit, and LTO is disabled), the calling convention may still require passing a byte for the empty class to satisfy the ABI. This is a minor, usually irrelevant cost. For header-only or inlined usage — which is the common case — there is no overhead at all.
Compile-time cost
Slightly more template instantiation (one specialization per authorized type), and slightly longer function signatures. This is the real price, but it is usually trivial.
Readability trade-offs
- Signatures get noisier:
Widget(Passkey<Factory>, int)is visually heavier thanWidget(int). - The unnamed parameter convention (see below) is what keeps it readable; without it, reviewers wonder what the argument is for.
- The mechanism is compile-time enforced, so the compiler error when someone tries to bypass it is clear — but the reason the parameter exists is not obvious to a reader seeing the class for the first time. A brief comment on the constructor is worth the cost.
6. Idiomatic Usage and Best Practices
-
Leave the parameter unnamed. Write
void foo(Passkey<T>), notvoid foo(Passkey<T> key). The unnamed parameter signals "this is a token, not data — it carries no information and is never used in the body." -
Mark the constructor
explicitif you are being defensive.explicit Passkey() = default;is not strictly required — the private access already blocks unauthorized construction — but it guards against brace-initialization surprises and makes intent explicit. It is harmless and recommended for older-standard compatibility (C++17 or earlier). - Prefer the templated form. The nested form is fine as a teaching example, but the templated form is the one to use in production.
-
Document the contract. A one-line comment —
// only Factory may construct Widget— saves future readers from reverse-engineering the idiom.
8. Summary
-
friendis all-or-nothing; the PassKey idiom gives you per-function, per-caller access control. - The nested PassKey is the minimal teaching form; the templated
Passkey<T>is the idiomatic production form. -
C++26 variadic friends (
friend Ts...) turns "group permissions" into a one-liner, removing the last piece of friction. - It is a zero-cost abstraction in optimized builds; the cost is compile-time and readability, not runtime.
- Use it for restricted constructors and sensitive methods; use Attorney-Client when you need to expose members instead.
Happy coding!
Top comments (0)