Attorney-Client
Interpose a friend proxy class to grant selective, fine-grained access to a subset of another class's private members.
Attorney-Client Idiomsince C++98An intermediate "Attorney" class, declared friend of the "Client", re-exports only a chosen subset of the client's private interface as its own private static methods, which it then selectively grants to specific callers.
Overview
C++ friendship is binary and total: a friend declaration gives the named class
unrestricted access to every private and protected member of the class granting
it. When you need a third party to touch exactly one private method—say, a
serialisation engine that must reach an internal buffer, or a test harness that
needs to reset cached state—you are forced to either expose everything or
restructure the class.
The Attorney-Client idiom breaks that all-or-nothing constraint without modifying the client class's public interface and without scattering friendship declarations across unrelated translation units. A thin, dedicated Attorney class acts as a surgical gateway: it is the only friend of the Client, and it exposes precisely the private operations its own callers require, keeping everything else locked away.
The pattern composes naturally. One Client can have multiple Attorneys, each granting a different slice of access to a different audience. The Client itself never needs to know about the callers; it only knows about Attorneys.
Syntax
// Client.h
class Client {
public:
explicit Client(int value) : value_(value), calls_(0) {}
int public_value() const { return value_; }
private:
int value_;
int calls_;
void reset() { value_ = 0; calls_ = 0; }
int& mutable_value() { return value_; }
void record_call() { ++calls_; }
friend class ClientAttorney; // single, controlled gateway
};
// ClientAttorney.h
class ClientAttorney {
// All forwarding methods are private: only explicitly named
// friends of the Attorney may call them.
static void reset(Client& c) { c.reset(); }
static int& mutable_value(Client& c) { return c.mutable_value(); }
static void record_call(Client& c) { c.record_call(); }
friend class TestHarness; // gets reset()
friend class Instrumentation; // gets record_call()
// mutable_value() intentionally not exposed to anyone yet
};The static-method pattern is deliberate: Attorney instances are never created, preventing callers from storing a reference and calling through it later. All access is explicit, stateless, and auditable.
Examples
Test Harness Access
A common use case is white-box testing without making internal state part of the public API.
// test_client.cpp
#include "Client.h"
#include "ClientAttorney.h"
#include <cassert>
class TestHarness {
public:
static void run_reset_test() {
Client c{42};
assert(c.public_value() == 42);
ClientAttorney::reset(c); // compiles; TestHarness is attorney's friend
assert(c.public_value() == 0);
}
};
int main() {
TestHarness::run_reset_test();
}Instrumentation cannot call reset because it is not listed as a friend of
ClientAttorney for that particular static method—all three methods are private,
but only TestHarness is named alongside reset's logical grouping.
Per-Caller Granularity
When multiple callers each need a different capability, use separate Attorney classes or express the intent clearly inside one:
class SerializerAttorney {
static const int* raw_buffer(const Client& c) {
return &c.value_; // only the fields serialisation legitimately needs
}
friend class BinarySerializer;
friend class JsonSerializer;
};
class MutatorAttorney {
static void set_value(Client& c, int v) { c.value_ = v; }
friend class MigrationTool;
};Each Attorney is a named declaration of intent: "these callers may touch this surface, no more." Code review and audits have a single place to inspect for each access boundary.
C++11: Variadic Forwarding in Attorneys
With C++11 perfect forwarding, an Attorney can forward to private constructors or methods that take arbitrary arguments without writing overloads:
class ClientFactoryAttorney {
template<typename... Args>
static Client make(Args&&... args) { // C++11 variadic template
return Client(std::forward<Args>(args)...); // C++11 std::forward
}
friend class ClientPool;
};Before C++11, you would need a separate overload for each arity.
C++17: if constexpr in Attorney Logic
Where the exposed operation varies by type, C++17 if constexpr can select
the right private path at compile time inside an Attorney without relying on
SFINAE gymnastics:
class TypedAttorney {
template<typename T>
static void store(Client& c, T value) {
if constexpr (std::is_integral_v<T>) { // C++17
c.value_ = static_cast<int>(value);
} else {
static_assert(false, "unsupported type");
}
}
friend class TypedSink;
};Multiple Attorneys for Different Caller Groups
A single Client class often has several distinct cross-cutting concerns that
each require their own private surface. Rather than granting one large Attorney
access to everything, create one narrow Attorney per concern. The Client only
needs one friend declaration per Attorney, and each downstream caller sees
only what its Attorney exposes.
The example below models an Engine class used in a server application. A
SerializationAttorney lets a persistence layer read raw internal buffers
without exposing mutable state. A TestingAttorney lets unit tests reset
counters and inject artificial state. Neither Attorney can use the other's
granted methods:
// Engine.h – C++11
#include <cstdint>
#include <span> // C++20, use const uint8_t* + size_t for C++17
class Engine {
public:
explicit Engine(int capacity);
void process(const uint8_t* data, std::size_t len);
int processed_count() const { return processed_count_; }
private:
int capacity_;
int processed_count_ = 0;
bool healthy_ = true;
uint8_t internal_buf_[4096];
// --- read-only access for the serialisation layer ---
const uint8_t* raw_buf() const { return internal_buf_; }
std::size_t raw_buf_len() const { return sizeof(internal_buf_); }
// --- write access for the test harness ---
void reset_counters() { processed_count_ = 0; }
void inject_unhealthy() { healthy_ = false; }
friend class SerializationAttorney; // read-only buffer access
friend class TestingAttorney; // reset + inject
};The two Attorneys are in separate headers. Production code only needs to
include Engine.h and SerializationAttorney.h; the test binary additionally
includes TestingAttorney.h. Neither header leaks into the other's consumers.
// SerializationAttorney.h – C++11
#include "Engine.h"
class SerializationAttorney {
// Read-only: the serialisation engine has no business mutating Engine state.
static const uint8_t* raw_buf(const Engine& e) { return e.raw_buf(); }
static std::size_t raw_buf_len(const Engine& e) { return e.raw_buf_len(); }
friend class BinarySerializer;
friend class CheckpointWriter;
// TestingAttorney is NOT listed: it cannot call raw_buf / raw_buf_len
};// TestingAttorney.h – C++11
#include "Engine.h"
class TestingAttorney {
// Mutable access intentionally limited to test infrastructure.
static void reset_counters(Engine& e) { e.reset_counters(); }
static void inject_unhealthy(Engine& e){ e.inject_unhealthy(); }
friend class EngineTests;
friend class FaultInjectionHarness;
// SerializationAttorney is NOT listed: it cannot reset or inject
};A BinarySerializer that accidentally #includes TestingAttorney.h still
cannot call reset_counters because it is not named in TestingAttorney's
friend list. Header inclusion is a necessary but not sufficient condition for
access — the explicit friend declaration is the actual guard.
Attorney as a Namespace-Scope Class: Header Placement
The Attorney must be a complete type at the point the Client declares it as a
friend, so the two classes share a dependency order to manage carefully.
Option A — Attorney in the same header as the Client.
Simple to set up; every consumer of Client.h sees the Attorney's interface.
Appropriate when the Attorney serves production code consumed broadly (e.g., a
serialisation attorney used in every translation unit that touches the client).
// Client.h – Attorney declared in same header
class Client {
// ...
friend class ClientAttorney;
};
class ClientAttorney {
static void reset(Client& c) { c.reset(); }
friend class SomeProductionSubsystem;
};Option B — Attorney in a separate header, forward-declared in Client.h.
Client.h forward-declares the Attorney (no definition needed for a friend
declaration); the full Attorney definition lives in ClientAttorney.h. Only
translation units that #include "ClientAttorney.h" can use it.
// Client.h – forward declaration only
class ClientAttorney; // forward declaration; full definition not required here
class Client {
// ...
friend class ClientAttorney;
};// ClientAttorney.h – full definition; include only where needed
#include "Client.h"
class ClientAttorney {
static void reset(Client& c) { c.reset(); }
friend class Subsystem;
};Option C — Test-only header, excluded from production builds.
For Attorneys that exist solely for testing, place them in a test/ or
include/testing/ directory and ensure the production CMake target never
lists that directory in its include paths. This makes it physically impossible
for production code to even see the Attorney, not just logically constrained:
# CMakeLists.txt
target_include_directories(engine PRIVATE src/include) # no test/ here
target_include_directories(engine_tests PRIVATE src/include test/include)Option B is usually the best default because it minimises header fan-out while keeping the friend declaration in the canonical place (the client header).
Combining with the Passkey Idiom
The Attorney-Client idiom prevents unauthorized classes from calling Attorney methods. It does not, by itself, prevent an authorized class from forwarding that ability to a third party through a public wrapper. The Passkey idiom closes that gap.
A Passkey is a private, unconstructible-from-outside type owned by the
Attorney. A method that takes a Passkey argument can only be called by code
that can construct a Passkey — which, because the constructor is private, is
only the Attorney itself (and types the Attorney explicitly befriends).
// C++11 — Attorney + Passkey composition
class Engine;
class TestingAttorney {
public:
// Passkey: private type, only TestingAttorney (and its friends) can construct it.
class Passkey {
friend class TestingAttorney;
Passkey() = default; // private constructor
};
// The method is PUBLIC on the Attorney, but callers must supply a Passkey.
// Only code that can obtain a Passkey (i.e., TestingAttorney itself) can call this.
static void reset_counters(Engine& e, Passkey);
// Helper: construct and immediately use the passkey — restricts the call
// to originate from inside TestingAttorney.
static void do_reset(Engine& e) {
reset_counters(e, Passkey{});
}
private:
friend class EngineTests;
};The downstream consumer calls through the Attorney's public helper, which manufactures the Passkey internally:
// EngineTests.cpp
class EngineTests {
public:
static void test_counter_reset() {
Engine e;
// TestingAttorney::reset_counters(e, ???) — cannot supply Passkey directly
TestingAttorney::do_reset(e); // this works; attorney holds the key
}
};This layering is rarely needed in practice, but it is useful when the Attorney itself is consumed by a wider audience and you want to guarantee that even within that audience, only the Attorney's own logic decides when a sensitive operation executes, rather than letting callers trigger it at arbitrary moments.
Real-World Use Cases
Serialization Engine Accessing Private State
A persistence layer needs to snapshot an object's internal buffers or field values to write them to disk. Making those fields public would widen the API permanently and invite unrelated code to depend on storage layout details. The Attorney exposes only read-only views:
// ConnectionPool.h
class ConnectionPool {
public:
ConnectionPool(int max_conns);
Connection acquire();
void release(Connection);
private:
int max_conns_;
int active_count_ = 0;
float avg_wait_ms_ = 0.0f;
// internal slot list elided for brevity
const int& snapshot_max() const { return max_conns_; }
int snapshot_active() const { return active_count_; }
float snapshot_wait() const { return avg_wait_ms_; }
friend class ConnectionPoolDiagnosticsAttorney;
};
// DiagnosticsAttorney.h
class ConnectionPoolDiagnosticsAttorney {
static int active(const ConnectionPool& p) { return p.snapshot_active(); }
static float avg_wait(const ConnectionPool& p){ return p.snapshot_wait(); }
static int max(const ConnectionPool& p) { return p.snapshot_max(); }
friend class MetricsExporter;
friend class HealthCheckEndpoint;
};MetricsExporter gets read access to internal counters without those counters
becoming part of ConnectionPool's public contract. A future refactor of
ConnectionPool that changes internal field names only requires updating the
Attorney forwarding functions — all call sites remain unchanged.
Test Harness Resetting Internal Counters
Many classes accumulate state—call counts, error tallies, cached results—that
must be zeroed between unit test cases. Exposing a reset() method publicly
risks callers using it in production code and invalidating invariants. Routing
it through a TestingAttorney keeps it invisible to production consumers:
// RateLimiter.h
class RateLimiter {
public:
RateLimiter(int requests_per_second);
bool try_acquire(); // public: allows one request through or returns false
private:
int limit_;
int count_this_window_ = 0;
bool window_expired_ = false;
void reset_window() { count_this_window_ = 0; window_expired_ = false; }
friend class RateLimiterTestingAttorney;
};// RateLimiterTestingAttorney.h (lives under test/include/, not production headers)
#include "RateLimiter.h"
class RateLimiterTestingAttorney {
static void reset_window(RateLimiter& rl) { rl.reset_window(); }
friend class RateLimiterTests;
};// RateLimiterTests.cpp
#include "RateLimiter.h"
#include "RateLimiterTestingAttorney.h"
#include <cassert>
class RateLimiterTests {
public:
static void test_rejects_when_full() {
RateLimiter rl{3};
assert(rl.try_acquire());
assert(rl.try_acquire());
assert(rl.try_acquire());
assert(!rl.try_acquire()); // fourth attempt: rejected
RateLimiterTestingAttorney::reset_window(rl); // reset for next test case
assert(rl.try_acquire()); // allowed again after reset
}
};Policy Component Reading Internal Config
A policy object may need to observe a class's internal configuration values
to make routing decisions, without those config values needing to be public.
This is common in plugin architectures where a plugin host exposes a narrow
PolicyAttorney to loaded plugins:
// Scheduler.h
class Scheduler {
public:
void enqueue(Task t);
void run_one();
private:
int thread_count_;
int queue_depth_limit_;
bool prefer_fifo_;
int thread_count() const { return thread_count_; }
int queue_depth_limit()const { return queue_depth_limit_; }
bool prefer_fifo() const { return prefer_fifo_; }
friend class SchedulerPolicyAttorney;
};
// SchedulerPolicyAttorney.h
class SchedulerPolicyAttorney {
static int thread_count(const Scheduler& s) { return s.thread_count(); }
static int queue_depth_limit(const Scheduler& s) { return s.queue_depth_limit(); }
static bool prefer_fifo(const Scheduler& s) { return s.prefer_fifo(); }
friend class PriorityPolicy;
friend class FairSharePolicy;
};PriorityPolicy can read the scheduler's configuration to calibrate its
decisions, but it cannot modify it, and unrelated subsystems never see those
fields at all.
Framework Hooking into Lifecycle Callbacks
A framework may need to fire private lifecycle hooks on managed objects—
on_before_persist(), on_after_load(), on_context_switch()—without
exposing those hooks as callable public API. The Attorney acts as the framework's
exclusive channel into the hook:
// ManagedObject.h
class ManagedObject {
public:
virtual ~ManagedObject() = default;
virtual void update() = 0;
private:
virtual void on_before_persist() {} // optional hook, private
virtual void on_after_load() {} // optional hook, private
friend class PersistenceFrameworkAttorney;
};// PersistenceFrameworkAttorney.h
class PersistenceFrameworkAttorney {
static void before_persist(ManagedObject& obj) { obj.on_before_persist(); }
static void after_load(ManagedObject& obj) { obj.on_after_load(); }
friend class PersistenceEngine;
};Only PersistenceEngine can trigger the lifecycle hooks. User-defined
subclasses of ManagedObject override them freely (they are private virtual,
which is valid C++), but nothing outside the framework can invoke them directly.
Template Attorney
When the set of operations to forward varies by a caller type parameter, a
template Attorney allows different instantiations to unlock different private
surfaces. The primary template can be left undefined (or defined with a
static_assert), and explicit specialisations provide only the operations
each policy needs.
// C++11
// Engine.h — primary template is intentionally undefined
template<typename Policy>
class EngineAttorney; // declaration only; undefined by default
class Engine {
public:
void run();
private:
int processed_ = 0;
float load_avg_ = 0.f;
bool healthy_ = true;
void reset() { processed_ = 0; load_avg_ = 0.f; }
float load_avg() const { return load_avg_; }
bool is_healthy() const { return healthy_; }
void set_healthy(bool v) { healthy_ = v; }
template<typename P>
friend class EngineAttorney; // all specialisations are friends
};Now define each specialisation in the header where it is needed:
// MonitoringPolicy.h
struct MonitoringPolicy {};
template<>
class EngineAttorney<MonitoringPolicy> {
// Read-only: monitoring only observes
static float load_avg(const Engine& e) { return e.load_avg(); }
static bool is_healthy(const Engine& e) { return e.is_healthy(); }
friend class MonitoringSubsystem;
friend class AlertingSubsystem;
};// TestingPolicy.h (test-only header)
struct TestingPolicy {};
template<>
class EngineAttorney<TestingPolicy> {
// Full write access for tests
static void reset(Engine& e) { e.reset(); }
static void set_healthy(Engine& e, bool v) { e.set_healthy(v); }
static float load_avg(const Engine& e) { return e.load_avg(); }
friend class EngineTestSuite;
};Callers use the concrete specialisation, not the primary template:
// MonitoringSubsystem.cpp
#include "Engine.h"
#include "MonitoringPolicy.h"
class MonitoringSubsystem {
public:
void report(const Engine& e) {
float load = EngineAttorney<MonitoringPolicy>::load_avg(e);
bool ok = EngineAttorney<MonitoringPolicy>::is_healthy(e);
// log(load, ok);
}
};Template specialisation caveat. The friend class EngineAttorney<Policy>
declaration in Engine must grant friendship to all specialisations at once
(via template<typename P> friend class EngineAttorney), or name each
specialisation individually. Granting all specialisations friendship is the
common choice, since the undefined primary template prevents instantiation of
an unauthorised specialisation—any EngineAttorney<Foo> without an explicit
specialisation definition will produce a compile error when its static methods
are called.
Comparison with Alternatives
When deciding whether to use the Attorney-Client idiom, weigh it against the other mechanisms C++ provides for controlled access.
protected Inheritance
Marking a member protected (or deriving privately and re-exporting via a
using declaration) gives every subclass access. This is appropriate when
the access relationship is genuinely hierarchical — a base class exposing
building blocks to derived implementations. When the "caller" is not a
logical subtype of the "client", inheritance creates a semantic lie and often
unwanted coupling to the base class layout.
friend class X (Direct Friendship)
Direct friendship grants the named class access to every private and protected
member, present and future. It provides no defence against accidental use of
an irrelevant private method, and it requires the client to know about each
caller by name, scattering cross-cutting concerns into the core class. The
Attorney-Client idiom builds on friend but restricts the blast radius to
a single Attorney class per concern.
Public Accessor Methods
Adding a get_raw_buf() accessor makes the interface permanently part of the
public API and visible to every consumer, including downstream dependencies
that have no business touching it. It also invites the accessor to be
extended over time — "as long as we have get_raw_buf(), let's also add
get_capacity() and get_checksum()" — growing the public surface steadily.
The Attorney pattern avoids this pressure because access is granted through a
separate class that only affects explicitly listed callers.
Comparison Table
| Mechanism | Access scope | Coupling | Auditable |
|---|---|---|---|
| Public accessor | Every caller | None (API stable) | No: any code can call it |
friend class X | All of Client's privates | High (client knows X) | At friend declaration |
protected member | All subclasses | Inheritance | At class definition |
| Attorney-Client | Selected operations only | Low (client knows Attorney) | At Attorney definition |
| Attorney + Passkey | Operations + call origin | Low | At both sites |
Limitations
Attorneys must be complete types before use. The full Attorney definition
must be visible in any translation unit that calls Attorney methods. If
Client.h only forward-declares the Attorney and a .cpp file includes
Client.h but not ClientAttorney.h, it cannot use the Attorney. This is a
feature (you explicitly opt in), but it requires discipline to keep include
lists tidy.
The pattern adds one layer of indirection. Every Attorney call is an
extra function call compared to a direct public method. For non-inlined
Attorneys this is a real cost; for header-only Attorneys with inline static
methods the compiler typically eliminates it. Mark Attorney methods inline
explicitly (or define them in the header) when performance matters.
Template Attorneys require careful specialisation management. If the
primary template is left undefined (the recommended approach), forgetting to
write a specialisation for a new policy type produces an opaque linker error
rather than a clear diagnostic. A static_assert in the primary template
body provides a better error message:
// C++11
template<typename Policy>
class EngineAttorney {
// Produce a clear error instead of a linker failure for undefined policies.
static_assert(sizeof(Policy) == 0,
"EngineAttorney<Policy>: no specialisation exists for this Policy type. "
"Define one in the appropriate header.");
};Friendship is not inherited. If Client declares EngineAttorney a
friend, a class DerivedEngine : public Engine does not automatically grant
that friendship to EngineAttorney. Derived classes must declare their own
friend if the Attorney needs access to their additional private members.
Access to the base class's private members continues to work through the
base class friend declaration, but only via base class methods — not through
direct access to derived-class-specific fields.
Multiple Attorneys can obscure the full access surface. With several
Attorney classes, the complete picture of who can touch what private member
is spread across multiple headers. A central comment block in Client.h
listing all Attorney classes and their purposes helps future readers
reconstruct the intent at a glance.
Best Practices
Name Attorneys after their purpose, not their clients. SerializationAttorney,
TestingAttorney, MigrationAttorney communicate intent at a glance.
ClientFriend or ClientHelper do not.
Keep Attorneys header-only with inline statics. They contain no state, so
there is nothing to put in a .cpp file. Keeping them in headers maximises
inlinability and keeps the access contract visible where the Client header is
included.
One Attorney per concern, not per caller. If BinarySerializer and
JsonSerializer need the same private surface, they share one Attorney.
Creating a new Attorney per downstream class defeats the auditability the
pattern is meant to provide.
Prefer const overloads when the intent is read-only. Attorneys should
mirror the const-correctness of the underlying private members; do not grant
mutable access when read access suffices.
Document why the access exists. An Attorney is a deliberate design decision, not a workaround. A one-line comment stating the invariant the access relies on pays dividends in future audits.
Annotate the Client header with a summary of all Attorneys. When a client
has multiple Attorney classes spread across separate headers, add a brief
table near the friend declarations so that a reader scanning Client.h
immediately understands the full access picture:
class Engine {
// --- Access gateway summary ---
// SerializationAttorney (SerializationAttorney.h): read-only buffer snapshot
// TestingAttorney (test/include/TestingAttorney.h): reset + inject
// PolicyAttorney (PolicyAttorney.h): config observation for policies
friend class SerializationAttorney;
friend class TestingAttorney;
friend class PolicyAttorney;
};Common Pitfalls
Forgetting that Attorney friendship is not transitive. If Attorney is
a friend of Client and ThirdParty is a friend of Attorney, ThirdParty
cannot directly access Client's privates. It can only call Attorney's private
methods. This is a feature, but surprises engineers who expect friendship to
chain.
Leaking the Attorney header unnecessarily. If Client.h includes
ClientAttorney.h, every consumer of Client.h sees the Attorney interface.
Forward-declare Attorney in Client.h and include ClientAttorney.h only
where it is needed.
Making Attorney methods public. Once they are public, anyone can call them,
and the controlled-access guarantee is gone. They must remain private with
explicit friend declarations for each authorized caller.
Using inheritance instead of static methods. Deriving from Attorney to gain access introduces lifetime coupling and allows callers to create Attorney instances, which opens the door to misuse. Static-only methods eliminate the object identity entirely.
Proliferating Attorneys until they outnumber real classes. If you find yourself writing an Attorney for every pair of classes, the underlying design probably needs revision. Attorneys solve a specific, bounded problem; overuse is a smell that the class boundary is wrong.
Granting friend class EngineAttorney to all template specialisations
unintentionally. The declaration template<typename P> friend class EngineAttorney makes every possible specialisation a friend, including ones
defined by third-party code or accidentally instantiated. Leave the primary
template undefined and use a static_assert as described in the Limitations
section to catch unsanctioned specialisations at compile time.
See Also
frienddeclarations — the foundational mechanism this idiom builds upon- Passkey idiom — an alternative that threads a dummy token type through the call site rather than a dedicated proxy class; lighter-weight for single-method access
- Pimpl (pointer-to-implementation) — moves private state into an opaque implementation struct, often eliminating the need for controlled friend access altogether