Skip to content
C++
Idiom
Intermediate

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++98

An 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

cpp
// 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.

cpp
// 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:

cpp
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:

cpp
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:

cpp
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;
};

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.

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.

See Also

  • friend declarations — 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