Idioms Attorney-Client and Passkey for selective access to class methods

When designing applications in C ++, sometimes it becomes necessary to provide access to the private methods of a class to another class or a free function. To do this, the C ++ language has the friend keyword, which provides full access not only to the public interface of the class, but also to the private and all implementation details. Thus, friend works on the principle of "all or nothing" and "all" may be too much. For example, when there is a Facade class and several clients Client1, Client2, it may be necessary to provide each client with access only to a specific set of methods, and each client to have its own set without providing access to implementation details. To solve such a problem in C ++ there are all the possibilities. In this article I will talk about two idioms Attorney-Client and Passkey and how to use them with zero overhead.

So the task is this: there are classes Server, Client and Intruder. The client should access Server :: some_method (), but not the implementation details. At the same time, Intruder should not gain access to the Server.

class Server
{
private: // закрытый интерфейс
    void some_method(); // метод для Client
    void one_more_method(); // этот метод должен остаться закрытым
private:
    // далее детали реализации класса...
};
class Client;
class Intruder;

Attorney-client


The Attorney-Client idiom is simpler and more straightforward, but long - let's start with it. To provide the required access to the Client, you can’t just make it a friend of the Server (it will get access to the entire contents of the server), you can’t just make the required method public (the cracker will also have access to it). In this situation, a trusted intermediary comes to the rescue, or rather Attorney.

class Attorney;

The trust chain will be organized this way: Client will be a friend of Attorney, and that will be another Server. The Attorney class will have a private inline static method proxying requests to the Server.


class Server
{
private: // закрытый интерфейс
    void some_method(); // метод для Client
    void one_more_method(); // этот метод должен остаться закрытым
private:
    // далее детали реализации класса...
    friend class Attorney;
};
class Attorney
{
private:
    static void proxy_some_method( Server& server )
    {
        server.some_method();
    }
    friend class Client;
};
class Client
{
private:
    void do_something(Server& server);
};
void Client::do_something( Server& server )
{
    // server.some_method(); // <- так не сработает
    Attorney::proxy_some_method( server );
    // server.one_more_method(); // <- этот метод тоже не доступен
}
class Intruder
{
private:
    void do_some_evil_staff( Server& server )
    {
        // server.some_method(); // <- это не сработает
    }
};

  • The proxy methods in the lawyer class must be inline, then any optimizer will delete them and will directly call the methods of the CardAccount class. It’s quite easy to check by copying the code to godbolt and compare the generated code for the version with proxy_some_method () and direct call (changing private to public).

  • Access to private methods can also be provided with a free function. To do this, you need to appoint her a friend in the lawyer class.

Passkey


The second way to provide selective access to a private interface is the Passkey idiom. It is shorter and the code is cleaner, therefore, I like it more, but a little more unobvious. The task is the same: Server, Client, Intruder, but this time the proxy methods are declared public, however, a special Passkey parameter with a private constructor is added to them, which can only be called by explicitly listed friends (classes, free functions). The Passkey parameter is utility, it is created immediately at the time the proxy function is called, and it is destroyed when it leaves it (this is a temporary object, it is not stored in a variable). As a result, void some_method (Passkey) can call only the class that can call the Passkey constructor (and all these classes are listed as Passkey friends).


class Server
{
public:
    class Passkey
    {
    private:
        friend class Client; // только Client сможет вызвать конструктор Passkey
        Passkey() noexcept {}
        Passkey( Passkey&& ) {}
    };
    void some_method( Passkey ) // экземпляр Passkey может создать только Client
    {
        some_method();
    }
private: // закрытый интерфейс
    void some_method(); // метод для Client
    void one_more_method(); // этот метод должен остаться закрытым
private:
    // далее детали реализации класса...
};
class Client
{
private:
    void do_something( Server& server );
};
void Client::do_something( Server& server )
{
    // server.some_method(); // <- так не сработает
    // server.one_more_method(); // <- этот метод тоже не доступен
    server.some_method( Server::Passkey() );
    // или так, если не возникает неопределенности при вызове перегруженных методов
    server.some_method( {} );
}
class Intruder
{
private:
    void do_some_evil_staff( Server& server )
    {
        // server.some_method(); // <- это не сработает
    }
};

To improve readability and get rid of duplication of the inclusion of Passkey class code in other classes, it can be made boilerplate and put into a separate header file.


template 
class Passkey
{
private:
    friend T;
    Passkey() noexcept {}
    Passkey( Passkey&& ) {}
    Passkey( const Passkey& ) = delete;
    Passkey& operator=( const Passkey& ) = delete;
    Passkey& operator=( Passkey&& )      = delete;
};

The only purpose of Passkey is to create a temporary instance and pass it to the proxy method, for this you need empty default constructor and move, all other constructors and assignment operators are forbidden (just in case, in order not to use Passkey for other purposes).


Final version
// === passkey.hpp
template 
class Passkey
{
private:
    friend T;
    Passkey() noexcept {}
    Passkey( Passkey&& ) {}
    Passkey( const Passkey& ) = delete;
    Passkey& operator=( const Passkey& ) = delete;
    Passkey& operator=( Passkey&& )      = delete;
};
// === server.hpp
class Client;
class SuperClient;
class Server
{
public:
    void proxy_some_method( Passkey ); // proxy для Client
    void proxy_some_method( Passkey ); // proxy для SuperClient
private: // закрытый интерфейс
    void some_method(); // метод для Client
    void one_more_method(); // этот метод должен остаться закрытым
private:
    // далее детали реализации класса...
};
inline void Server::proxy_some_method( Passkey )
{
    some_method();
}
inline void Server::proxy_some_method( Passkey )
{
    some_method();
}
// === client.hpp
class Client
{
private:
    void do_something( Server& server );
};
void Client::do_something( Server& server )
{
    // server.some_method(); // <- так не сработает
    // server.one_more_method(); // <- этот метод тоже не доступен
    server.proxy_some_method( Passkey() );
    // server.proxy_some_method( {} ); // <- на этот раз возникает неопределенность в перегруженных методах
}
// evil.hpp
class Intruder
{
private:
    void do_some_evil_staff( Server& server )
    {
        // server.some_method(); // <- это не сработает
        // server.proxy_some_method( Passkey() ); // и это тоже
        // server.proxy_some_method( {} ); // и это...
    }
};


The calling classes (Client, SuperClient) will again be able to call only each “their” public methods, for which they can construct the Passkey parameter. Details of the Server implementation are completely inaccessible to them, as well as “alien” methods.


  • In this version, the proxy functions should also be inline and just proxy the call further, in this case (after the optimizer works) no temporary Passkey <> object will be created and the overhead will be zero.

  • Passkey <> cannot be made a default argument, i.e. this option will not work:

class Server
{
public:
    void proxy_some_method( Passkey pass = Passkey() );
private:
    void some_method();
};

  • I called proxy methods with the proxy_ prefix for educational purposes only, to make it clearer.

Conclusion


The described idioms Attorney-Client and Passkey allow you to selectively provide access to private methods of the class. Both of these methods work with zero runtime overhead, however, they require writing additional code and make the class interface less obvious than using the friend keyword. Do you need to fence this whole garden in your project or is it not worth it - it's up to you.


Also popular now: