Is Your Friend in C++ a Good Friend?

Passkey Idiom and Hidden FriendsPhoto by Michael Dziedzic on UnsplashThe term “Passkey Idiom” caught my attention while I was revisiting “C++ Software Design” by Klaus Iglberger, in the section about testability design.The friend keyword in C++ introdu…

Passkey Idiom and Hidden Friends

Photo by Michael Dziedzic on Unsplash

The term “Passkey Idiom” caught my attention while I was revisiting “C++ Software Design” by Klaus Iglberger, in the section about testability design.

The friend keyword in C++ introduces coupling—often artificial—and is typically discouraged, even for testing purposes. However, the book highlights two exceptions where friend is appropriate:

  1. Idiomatic uses like the Passkey idiom
  2. Hidden friends (for ADL-friendly operator overloading).

Passkey Idiom

After researching online, I found the idea behind Passkey idiom is cool!

Instead of granting blanket access to all private members and methods, it provides friendship only at a functional level.

One common use case is when a class should only be created through a factory. The factory needs special access to the class’s constructor, while other code should be blocked from creating instances directly. This prevents bypassing important setup or tracking logic — like managing database connections or game objects in a scene.

In that case, we can make the constructor of the class private, but have the specified access through a key.

Consider a simple example below

class Connection {
public:
Connection(const uint16_t port, ConstructorKey): port_(port) {}
private:
void internalFunction(){};
uint16_t port_;
bool internal_flag_;
};

Instead of directly define friend class inside the Connectionclass, the idiom is to use indirection through the ConstructorKeyor the “passkey”.

class ConstructorKey {
friend class ConnectionFactory;
constexpr ConstructorKey(){};
};

Constructor of ConstructorKeyis purposely made private and only allow friend class ConnectionFactory to be able to call it.

Hence, the factory class would be able to use the passkey to instantiate Connectionobject without access to its private data or methods.

class ConnectionFactory {
public:
Connection createConnection(const uint16_t port) {
return Connection(port, {});
}
};

The usage pattern would typically appear as follows in application code:

ConnectionFactory factory;
Connection connection = factory.createConnection(1883);

The design explicitly prevents direct instantiation attempts:

Connection connection{123, {}}; 
// would fail to compile
// ConstructorKey::ConstructorKey()' is private within this context

Generalized Passkey Idiom

When the pass key idiom is generalized, it would look something like

class PasskeyBase { // deal with aggregate initialization madness
protected:
constexpr PasskeyBase() = default;
};

template <typename T>
class Passkey : PasskeyBase {
friend T;
constexpr Passkey() = default;
};

where PasskeyBaseadheres C++ Core guidelines

“C.35: A base class destructor should be either public and virtual, or protected and non-virtual”.

We can construct the keys using the template:

using ConstructorKey = Passkey<ConnectionFactory>;
using MethodKey = Passkey<OtherClass>;

// example of usage for method
// int OnlyFooCanAccess(Passkey<Foo>);

Important details

You might question why we need the base class. And that’s because of the “aggregate initialization madness”.

If without the PasskeyBaseor we simply change the previous definition of constructor ofConstructorKey to be

constexpr ConstructorKey() = default;

, the following would surprisingly compile:

Connection connection{123, {}};

because even though defaulted constructor are not accessible, it can be created via uniform initialization if it has no data members.

The key issue arises when the Passkey has no data members. While adding dummy data (as shown below) avoids the problem, this solution is not ideal since the dummy member remains indeterminate

template <typename T>
class Passkey {
friend T;
constexpr Passkey() = default;
int dummy_; // indeterminate
};

Beyond handling aggregate initialization, a production-ready Passkey implementation must also account for another edge case — preventing the following circumvention attempt:

ConstructorKey* hack = nullptr;
Connection hack_connection(80, *hack);

We should make it syntactically impossible to create ConstructorKey using dereferencing an uninitialized or null pointer, which is an undefined behaviour. Thus, we can either define or delete the copy constructor of ConstructorKey

constexpr ConstructorKey(const ConstructorKey& other) = default; // or delete

Although we can rely on compiler to delete the copy assignment operator and copy assignment constructor by defining either move assignment operator or move assignment constructor, in our case, we could follow Rules of Five to delete all since each Passkey should be unique by itself.

template <typename T>
class Passkey : PasskeyBase {
friend T;
constexpr Passkey() = default;
constexpr Passkey(const Passkey&) = delete; // copy assignment operator
constexpr Passkey& operator=(const Passkey&) = delete; // copy assignment constructor
constexpr Passkey(Passkey&&) noexcept = delete; // move assignment operator
constexpr Passkey& operator=(Passkey&&) noexcept = delete; // move assignment constructor
};
// reference
class ConstructorKey {
friend class ConnectionFactory;
constexpr ConstructorKey(){};
constexpr ConstructorKey(const ConstructorKey& other) = delete; // copy constructor
};

Hidden friend

For hidden friend, it’s just related to Argument-Dependent Lookup (ADL) which is a C++ rule that extends how the compiler searches for functions during overload resolution.

We should not pollute the global namespace by putting the friend function that overloads inside the struct or class such as:

class Coordinate {
public:
Coordinate(int x, int y) : x_(x), y_(y){};

friend std::ostream& operator<<(std::ostream& os,
const Coordinate& coordinate) {
return os << coordinate.x_ << " " << coordinate.y_;
}

private:
int x_;
int y_;
};

std::cout << Coordinate(2, 1) << std::endl;

Conclusion

Now you understand the good uses of friend – the Passkey idiom for controlled access and hidden friends for clean operator overloading.

Use them wisely, my friend!

If this piece was useful, consider following me on Medium to stay updated. Your support fuels my work. Thanks!


Is Your Friend in C++ a Good Friend? was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.


Print Share Comment Cite Upload Translate Updates
APA

Pin Loon Lee | Sciencx (2025-05-02T00:44:16+00:00) Is Your Friend in C++ a Good Friend?. Retrieved from https://www.scien.cx/2025/05/02/is-your-friend-in-c-a-good-friend/

MLA
" » Is Your Friend in C++ a Good Friend?." Pin Loon Lee | Sciencx - Friday May 2, 2025, https://www.scien.cx/2025/05/02/is-your-friend-in-c-a-good-friend/
HARVARD
Pin Loon Lee | Sciencx Friday May 2, 2025 » Is Your Friend in C++ a Good Friend?., viewed ,<https://www.scien.cx/2025/05/02/is-your-friend-in-c-a-good-friend/>
VANCOUVER
Pin Loon Lee | Sciencx - » Is Your Friend in C++ a Good Friend?. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/05/02/is-your-friend-in-c-a-good-friend/
CHICAGO
" » Is Your Friend in C++ a Good Friend?." Pin Loon Lee | Sciencx - Accessed . https://www.scien.cx/2025/05/02/is-your-friend-in-c-a-good-friend/
IEEE
" » Is Your Friend in C++ a Good Friend?." Pin Loon Lee | Sciencx [Online]. Available: https://www.scien.cx/2025/05/02/is-your-friend-in-c-a-good-friend/. [Accessed: ]
rf:citation
» Is Your Friend in C++ a Good Friend? | Pin Loon Lee | Sciencx | https://www.scien.cx/2025/05/02/is-your-friend-in-c-a-good-friend/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.