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

Passkey Idiom

Grant selective access to private constructors and methods using an unforgeable token type, replacing coarse-grained friend declarations.

Passkey Idiomsince C++11

A technique that restricts access to specific constructors or methods by requiring an unforgeable token type whose private constructor only the designated caller can invoke.

Overview

friend is binary: granting it gives a class access to every private member. This is rarely what you want. The passkey idiom threads the needle β€” it exposes one constructor or one method to one designated class, and nothing else.

The mechanism is a small token type templated on the privileged caller. Its constructor is private, with the caller named as a friend. Any method or constructor that should be privileged takes this token as a parameter. Since only the friend can construct the token, only the friend can supply the argument β€” the compiler enforces the restriction at the call site.

C++11 introduced first-class support for friend T where T is a type template parameter (previously ill-formed). The entire idiom depends on that language rule.

The token itself is an empty class. The compiler elides it entirely; there is zero runtime overhead.

Syntax

cpp
// C++11: friend T where T is a template parameter
template<typename T>
class Passkey {
    friend T;           // only T can construct this
    Passkey() {}        // user-provided body β€” critical for C++11–17 safety (see Pitfalls)
    Passkey(const Passkey&) = delete;
    Passkey& operator=(const Passkey&) = delete;
};

A guarded constructor or method simply adds Passkey<Caller> to its parameter list:

cpp
class Widget {
public:
    explicit Widget(int x, Passkey<Factory>);  // only Factory can call this
};

The caller constructs the token inline at the call site β€” no named variable needed, no storage:

cpp
class Factory {
public:
    Widget make(int x) {
        return Widget{x, Passkey<Factory>{}};  // constructs and passes immediately
    }
};

External code attempting Passkey<Factory>{} gets a compile error: the constructor is private.

Examples

Controlling Construction: Connection Pool

A Connection should only be created by ConnectionPool. A private constructor with a static create() would work if there is one factory; passkey scales to multiple authorized callers without exposing all private state.

cpp
class Connection {
public:
    // Only ConnectionPool may construct a Connection directly
    Connection(std::string host, int port, Passkey<ConnectionPool>)
        : host_{std::move(host)}, port_{port} {}

    // Public interface β€” anyone can use an existing connection
    void execute(std::string_view sql);
    bool is_alive() const;

private:
    std::string host_;
    int         port_;
    // internal state hidden from ConnectionPool
    void*       impl_ = nullptr;
};

class ConnectionPool {
    std::vector<Connection> pool_;
public:
    void grow(std::string host, int port) {
        pool_.emplace_back(std::move(host), port, Passkey<ConnectionPool>{});
    }
    Connection& acquire();
    void release(Connection&);
};

// Won't compile β€” Passkey<ConnectionPool>() is private:
// Connection c{"db.local", 5432, Passkey<ConnectionPool>{}};

ConnectionPool cannot touch Connection::impl_ β€” it can only call the public interface like anyone else. friend would have given it everything.

Controlling Method Access: System Lifecycle

Some operations are valid in exactly one context. Passkey makes that context explicit in the type signature rather than burying it in a comment.

cpp
class EventBus {
public:
    void subscribe(std::string event, std::function<void()> fn) {
        handlers_[std::move(event)].push_back(std::move(fn));
    }

    void dispatch(std::string_view event);

    // Clearing all state is a shutdown-only operation β€” only SystemCore may call this
    void clear_all(Passkey<SystemCore>) {
        handlers_.clear();
    }

private:
    std::unordered_map<std::string,
                       std::vector<std::function<void()>>> handlers_;
};

class SystemCore {
    EventBus& bus_;
public:
    explicit SystemCore(EventBus& b) : bus_{b} {}

    void shutdown() {
        // other teardown ...
        bus_.clear_all(Passkey<SystemCore>{});
    }
};

The guarded parameter appears in the documentation, autocomplete, and error messages. Reviewers can see the intent without reading comments.

Multiple Distinct Callers

When two unrelated classes need access to different parts of a type, give each its own passkey parameter on a separate overload or a separate method:

cpp
class Buffer {
public:
    // Loader fills the buffer during asset loading
    void fill(std::span<const std::byte> data, Passkey<Loader>);

    // Renderer reads raw bytes for upload to GPU
    const std::byte* raw(Passkey<Renderer>) const;

    // Public interface for everyone else
    std::size_t size() const;
    bool empty() const;

private:
    std::vector<std::byte> data_;
};

Each caller's access is confined to exactly the operation it needs. Neither Loader nor Renderer is a friend, so neither can touch data_ directly.

Best Practices

Use a user-provided constructor body, not = default. See Pitfalls for why = default is unsafe in C++11–17.

Delete copy and move. The token is a single-use key. Deleting copy prevents someone who receives a passkey argument from stashing it for later reuse, which would extend access beyond the intended scope.

cpp
template<typename T>
class Passkey {
    friend T;
    Passkey() {}
    Passkey(const Passkey&) = delete;
    Passkey& operator=(const Passkey&) = delete;
};

Put the passkey last. Placing Passkey<Owner> as the final parameter keeps it visually separated from the meaningful arguments and makes call sites self-documenting: Widget{width, height, Passkey<Factory>{}}.

Keep the token local. Construct Passkey<T>{} inline at the call site. Never store it in a variable or pass it through intermediate functions β€” that widens the access surface.

Prefer passkey over friend when: the privileged caller needs access to one or two specific entry points, multiple unrelated classes need access to different entry points, or the type has other private state that should remain hidden.

Common Pitfalls

Aggregate Initialization Bypass (C++11–17)

This is the most dangerous defect. If Passkey uses = default instead of a user-provided constructor body, it is an aggregate in C++11 through C++17:

cpp
// UNSAFE in C++11, C++14, C++17:
template<typename T>
class Passkey {
    friend T;
    Passkey() = default;  // not user-provided β†’ class is an aggregate
};

// Aggregate initialization bypasses the private constructor:
Passkey<Factory> leak{};  // compiles β€” no restriction enforced
Widget w{leak};           // access control defeated

C++20 changed the aggregate definition to exclude classes with any user-declared constructor (even = default), closing this hole. For code targeting C++11–17, use a body:

cpp
Passkey() {}  // user-provided body: class is never an aggregate

Wrong Template Argument

The compiler catches this, but the error message can be cryptic. If a function requires Passkey<Factory> and you pass Passkey<Widget>, you get a "constructor is private" diagnostic that points to Passkey's constructor rather than the call site. Keep template arguments close together in source to avoid confusion.

Leaking Through Forwarding Functions

A helper that forwards a passkey extends access beyond the intended owner:

cpp
// Dangerous: anyone can call make_widget and get Factory-level access
Widget make_widget(int x) {
    return Widget{x, Passkey<Factory>{}};  // only safe inside Factory
}

Passkey-constructing code belongs inside the privileged class, not in free functions or utilities that anyone can call.

See Also

  • CRTP β€” another template-based access-control technique, often combined with passkey for policy hierarchies
  • Factory Idiom β€” when a single factory owns construction and friend or private ctor + static create() is sufficient
  • PIMPL Idiom β€” hides implementation details entirely rather than selectively exposing them