📜 ⬆️ ⬇️

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

When designing C ++ applications, it is sometimes necessary to provide access to private class methods to another class or free function. To do this, the C ++ language has the friend keyword, which provides full access not only to the class's public interface, but also to the private and all implementation details. Thus a 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 its own set, without providing access to implementation details. To solve this problem in C ++ there are all the possibilities. In this article I will discuss the two idioms of Attorney-Client and Passkey and how to use them with zero overhead.

So the problem is this: there are classes Server, Client and Intruder. The client must access Server :: some_method (), but not the implementation details. However, Intruder should not access 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 rectilinear, but long - and let's start with it. To provide the Client with the required access, you cannot simply make it a friend of the Server (it will gain access to all server content), nor can you simply make the required method public (it will be accessed by the attacker). In such a situation, a trusted intermediary comes to the rescue, or rather Attorney.

 class Attorney; 

The chain of trust will be organized this way: Client will be Attorney's friend, and that Server's friend. The Attorney class will have a private inline static method that proxies 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(); // <-    } }; 


Passkey


The second way to provide selective access to a private interface is the Passkey idiom. It is shorter and the code is cleaner, so I like it more, but a little more obscure. The task is the same: Server, Client, Intruder, but this time the proxy methods are declared public, but they add a special parameter Passkey with a closed constructor, which can only be called by explicitly listed friends (classes, free functions). The Passkey is a service parameter, it is created immediately when the proxy function is called, and it is destroyed when it exits (it is a temporary object, it is not stored in a variable). As a result, void some_method (Passkey) can be called only by the class that the Passkey constructor can call (and all these classes are listed in Passkey's 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 template and put into a separate header file.


 template <typename T> 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 of it and pass it to the proxy method. This requires empty default constructor and displacements, all other constructors and assignment operators are prohibited (just in case, in order not to use Passkey for other purposes).


Final version
 // === passkey.hpp template <typename T> 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<Client> ); // proxy  Client void proxy_some_method( Passkey<SuperClient> ); // proxy  SuperClient private: //   void some_method(); //   Client void one_more_method(); //      private: //    ... }; inline void Server::proxy_some_method( Passkey<Client> ) { some_method(); } inline void Server::proxy_some_method( Passkey<SuperClient> ) { 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<Client>() ); // server.proxy_some_method( {} ); // <-         } // evil.hpp class Intruder { private: void do_some_evil_staff( Server& server ) { // server.some_method(); // <-    // server.proxy_some_method( Passkey<Client>() ); //    // server.proxy_some_method( {} ); //  ... } }; 


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



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


Conclusion


The Attorney-Client and Passkey idioms described allow selective access to private class methods. Both of these methods work with zero runtime overhead, however, they require writing additional code and make the class interface not so obvious as compared to using the friend keyword. Whether it is necessary to fence all this garden in your project or it is not worth it - this is up to you.

Source: https://habr.com/ru/post/326540/


All Articles