При проектировании приложений на C++ временами возникает необходимость предоставления доступа к закрытым методам класса другому классу или свободной функции. Для этого в языке C++ есть ключевое слово friend, которое предоставляет полный доступ не только к публичному интерфейсу класса, но и к закрытому, и всем деталям реализации. Таким образом friend работает по принципу «все или ничего» и «все» может быть слишком много. Например, когда есть класс Facade и несколько клиентов Client1, Client2, то может потребоваться предоставить каждому клиенту доступ только к определенному набору методов, причем каждому клиенту к своему набору, не предоставляя доступа к деталям реализации. Для решения такой задачи в C++ есть все возможности. В этой статье я расскажу про две идиомы Attorney-Client и Passkey и как их использовать с нулевыми накладными расходами.

Итак задача такая: есть классы Server, Client и Intruder. Клиент должен получить доступ к Server::some_method(), но не к деталям реализации. При этом Intruder не должен получить доступ к Server.

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

Attorney-Client


Идиома Attorney-Client более простая и прямолинейная, но длинная — с нее и начнем. Для предоставления Client требуемого доступа нельзя просто сделать его другом Server (он получит доступ ко всему содержимому сервера), также нельзя просто сделать требуемый метод публичным (к нему получит доступ и взломщик). В такой ситуации на помощь приходит доверенный посредник, а точнее Attorney.

class Attorney;

Цепочка доверия будет организована таким образом: Client будет другом Attorney, а тот другом Server. В классе Attorney будет закрытый inline static метод, проксирующий запросы к 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(); // <- это не сработает
    }
};

  • Прокси методы в классе-адвокате должны быть inline, тогда любой оптимизатор их удалит и будет напрямую вызывать методы класса CardAccount. Это довольно легко проверить скопипастив код на godbolt и сравнить генерируемый код для варианта с proxy_some_method() и прямого вызова (поменяв private на public).

  • Доступ к закрытым методам можно предоставлять и свободной функции. Для этого нужно ее назначить другом в классе-адвокате.

Passkey


Второй способ предоставления выборочного доступа к закрытому интерфейсу — идиома Passkey. Она короче и код получается чище, поэтому, мне нравится больше, но чуть более неочевидная. Задача та же: Server, Client, Intruder, но на этот раз прокси методы объявляются публичными, однако к ним добавляется специальный параметр Passkey с закрытым конструктором, который может быть вызван только явно перечисленными друзьями (классами, свободными функциями). Параметр Passkey служебный, его создают непосредственно в момент вызова прокси-функции и он уничтожается при выходе из нее (это временный объект, его не сохраняют в переменную). В результате void some_method( Passkey ) может вызвать только тот класс, который сможет вызвать конструктор Passkey (а все эти классы перечислены в друзьях Passkey).


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(); // <- это не сработает
    }
};

Для улучшения читаемости и избавления от дублирования включения кода класса Passkey в других классах, его можно сделать шаблонным и вынести в отдельный заголовочный файл.


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

Единственное назначение Passkey в том, чтобы создать его временный экземпляр и передать в proxy метод, для этого нужны пустые конструктор по умолчанию и перемещения, все остальные конструкторы и операторы присваивания запрещены (на всякий случай, чтобы не использовать Passkey не по назначению).


Окончательный вариант
// === 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( {} ); // и это...
    }
};


Вызывающие классы (Client, SuperClient) опять же смогут вызвать только каждый “свои” публичные методы, для которых смогут сконструировать параметр Passkey. Детали реализации Server им совсем недоступны, как и “чужие” методы.


  • В этом варианте прокси-функции также должны быть inline и просто проксировать вызов дальше, в таком случае (после работы оптимизатора) никакой временный объект Passkey<> создаваться не будет и накладные расходы будут нулевыми.

  • Passkey<> нельзя делать аргументом по умолчанию, т.е. такой вариант не сработает:

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

  • Прокси методы я назвал с префиксом proxy_ исключительно для учебных целей, чтобы было понятнее.

Заключение


Описанные идиомы Attorney-Client и Passkey позволяют выборочно предоставить доступ к закрытым методам класса. Оба эти способа работают с нулевыми накладными расходами времени выполнения, однако, требуют написания дополнительного кода и делают интерфейс класса не таким очевидным, по сравнению с использованием ключевого слова friend. Нужно ли городить весь этот огород в Вашем проекте или оно того не стоит – это уже решать Вам.

Поделиться с друзьями
-->

Комментарии (94)


  1. Videoman
    14.04.2017 17:00

    Во всех подобных случаях, гораздо полезней не запрещать, а скрывать реализацию.

    например, когда есть класс Facade и несколько клиентов Client1, Client2, то может потребоваться предоставить каждому клиенту доступ только к определенному набору методов, причем каждому клиенту к своему набору, не предоставляя доступа к деталям реализации...

    Оба «паттерна», на самом деле ничего не скрывают. Оба клиента, по-прежнему, видят все «кишки» и зависят друг от друга. А нужно, и всего-то, просто предоставить два интерфейса: InterfaceForClient1, и InterfaceForClient2. В этом случае вы действительно решите поставленную цель.


    1. eao197
      14.04.2017 17:59

      А нужно, и всего-то, просто предоставить два интерфейса: InterfaceForClient1, и InterfaceForClient2
      Чем это отличается от создания отдельных Attorney для каждого из клиентов?


      1. Videoman
        14.04.2017 18:13

        Так если ничем не отличается, зачем плодить новые сущности без необходимости. Про бритву Оккама слышали.


        1. eao197
          14.04.2017 18:16

          Так если ничем не отличается
          Простите, но это я у вас хотел узнать: отличаются или не отличаются? Можете на этот вопрос ответить?


          1. Videoman
            14.04.2017 18:29

            Attorney у вас функционально не отличается, но вот Server-у это ничем не поможет, к сожалению. У вас там по прежнему будут методы и для Client1 и для Client2 и все их параметры (и может быть десятки типов этих параметров). Ну т.е. никакого сокрытия реализации не получается в таком случае, меняется Server и «поехали» все перекомпилировать по новой…


            1. eao197
              14.04.2017 18:35

              С чего бы пользователь класса Attorney должен был бы видеть все потроха класса Server? Все может быть вообще вот в таком виде:

              class Server; // Это все, что видно снаружи.
              class Attorney {
              public:
                void some_method(Server &srv);
                void some_another_method(Server &srv);
              ...
              };
              Все детали реализации Attorney и Server могут быть упрятаны в *.cpp-файлы, которые клиенту не видны.


              1. Videoman
                14.04.2017 18:42

                А как вот этот тогда вызов сделать:

                class Client
                {
                private:
                    void do_something(Server& server);
                };
                
                ???
                Если у вас Server вообще не виден, тогда это интерфейс и у вас вызов интерфейса через интерфейс. И зачем тогда так код запутывать???


                1. eao197
                  14.04.2017 18:50

                  А как вот этот тогда вызов сделать:

                  void Client::do_something(Server& server) {
                    Attorney::do_something(server);
                    ...
                  };
                  
                  Определите, пожалуйста, свое понимание «интерфейс», а то есть ощущение, что мы о совсем разных вещах говорим.

                  Практическое применение, например, может быть при использовании идиомы PImpl:
                  class Server {
                  public:
                    void first_public_method(...);
                    void second_public_method(...);
                    ...
                  private:
                    class Impl;
                    std::unique_ptr<Impl> impl_;
                  };
                  
                  И классы Attorney имеют возможность обращаться к потрохам Server::impl_.
                  Еще один вариант, когда публичные методы у Server невиртуальные, а приватные методы — виртуальные. И для выполнения каких-то действий снаружи нужно обращаться напрямую к приватным виртуальным методам. Как раз классы Attorney смогут это сделать.


                  1. Videoman
                    14.04.2017 19:15

                    Послушайте, я не утверждаю, что ваш «паттерн» совсем нигде не применим. Просто хочется, чтобы задача, которую он решает, и границы его применения были бы озвучены четче, вот и все. Очевидно что он ничего не изолирует, а точнее изолирует также как и friend и private, т.е. очень слабо. В ответ вы добавляете Pimpl. Pimpl — изолирует детали реализации, безусловно, но это уже другой «паттерн». Если это просто, еще один, способ запретить вызвать из одного класса, методы другого класса, то смысла в этом маловато, по моему мнению.


                    1. eao197
                      14.04.2017 20:15

                      Во-первых, это не мой «паттерн».

                      Во-вторых, хотелось бы все таки услышать ответ на прямой вопрос.


                      1. Videoman
                        14.04.2017 21:09

                        Функционально — ничем. Только запутанней и сложнее и в случае чего сложнее для поддержки. Интерфейс дает ту же функциональность, только гибче, без потери возможностей.


                        1. eao197
                          14.04.2017 21:10

                          Пока вы не покажете хотя бы приблизительный код — это все голословно.


            1. Algoritmist
              14.04.2017 18:38

              Если интерфейс уникальный для каждого клиента, то все равно надо будет добавляеть его в объявлении класса Server, и перекомпилировать (или я что-то упустил?).


            1. masterspline
              14.04.2017 18:49

              Общение будет более конструктивным, если ты предложишь свой вариант и объяснишь в каких случаях и почему он лучше (уверен, что не всегда).


          1. Algoritmist
            14.04.2017 18:33

            Что-то мне шаблон понравился. Через интерфейсы надо виртуальные функции делать. Следовательно, эта проверка потянет накладные расходы (это крохи, но ...), а пока все вопросы решаются на этапе компиляции.


            1. eao197
              14.04.2017 18:38

              Через интерфейсы надо виртуальные функции делать.
              В C++, насколько я знаю, нет понятия «интерфейс» на уровне языка. Не обязательно речь должна идти об абстрактных типах, содержащих только чистые виртуальные методы.


              1. Videoman
                14.04.2017 18:51

                Ну вот не нужно только запутывать все. Естественно интерфейс имеет широкое понятие и т.д. и т.п. Под интерфейсов в «паттернах» (о чем вы пишите), всегда понимается контракт вызовов, оторванный от деталей реализации. Иначе никакого «сокрытия» не получится (или вы не раскрыли термин) или оно будет не полным.


                1. eao197
                  14.04.2017 18:57

                  Ну вот не нужно только запутывать все.
                  Мы в теме про C++ находимся. Здесь изначально все запутано. Вот, например:
                  class ComplexNumber {
                  public:
                    ComplexNumber& add(const ComplexNumber & other);
                    ...
                  };
                  ComplexNumber operator+(const ComplexNumber &a, const ComplexNumber &b) {
                    ComplexNumber r{a};
                    r.add(b);
                    return r;
                  }
                  
                  Здесь operator+ будет частью интерфейса класса ComplexNumber.

                  В связи с этим уточните, когда вы говорите:
                  А нужно, и всего-то, просто предоставить два интерфейса: InterfaceForClient1, и InterfaceForClient2
                  вы что именно подразумеваете?


                  1. Videoman
                    14.04.2017 19:21

                    Отлично! Ну и напишем, что данная реализация интерфейса класса ComplexNumber защищает вас от неправильного изменения внутреннего состояния класса, от порядка вызова методов класса. От появления внешних зависимостей и от перекомпиляции кода использующего данный класс в случае его изменения, данная реализация не защищает.


                    1. eao197
                      14.04.2017 20:13
                      -1

                      Отлично что? Мой пример всего лишь показывает, что «интерфейс» в C++ — это более размытое понятие, чем интерфейс в какой-нибудь Java. Посему ваша фраза «А нужно, и всего-то, просто предоставить два интерфейса: InterfaceForClient1, и InterfaceForClient2» для C++ников не такая очевидная, как вам может показаться. Поэтому, пожалуйста, будьте добры, проиллюстрируйте свое утверждение про InterfaceForClient1 и InterfaceForClient2 каким-то примером кода.


                      1. Videoman
                        14.04.2017 21:06

                        Ок. Тут вопрос не в иллюстрации, а в том, чего вы хотите добиться? Какую задачу вы решаете? Вам не кажется, что предоставлять публичный интерфейс и при этом его ограничивать для определенных классов это, мягко говоря, криво и большой косяк архитектуры? Если у вас возникает такая задача, то ваши классы очень сильно связаны, а это очень плохо, с точки зрения архитектуры, так как умножает количество мест, где в случае каких-либо изменений нужно будет менять код.


                        1. eao197
                          14.04.2017 21:09

                          а в том, чего вы хотите добиться?
                          Простого ответа на простой вопрос. Поскольку есть ощущения, что либо я не понимаю о чем говорите вы, либо вы не понимаете, какие проблемы решают подходы из статье.

                          Пожалуйста, приведите иллюстрацию. Ибо есть подозрение, что лучше, чем вот это, у вас не получится.


                          1. Videoman
                            14.04.2017 21:41

                            Вы хотите что бы я привел аналогичный интерфейс для абстрактного примера, ну ладно. Допустим в лоб.

                            class Server
                            {
                            public:
                            
                            InterfaceForClient1& GetInterfaceForClientType1();
                            InterfaceForClient2& GetInterfaceForClientType2();
                            
                            private:
                            
                            //...
                            }
                            


                            1. eao197
                              14.04.2017 21:45

                              Ну и:

                              1. Кто будет мешать дергать GetInterfaceForClientType* всем, кому не лень? Где гарантия того, что GetInterfaceForClientType1 будет вызван именно первым клиентом?

                              2. Как все это будет защищать от перекомпиляций при изменении потрохов Server?

                              3. Где и как будут создаваться экземпляры InterfaceForClient? Кто и как будет гарантировать время их жизни?


                              1. Videoman
                                14.04.2017 22:05
                                +1

                                Кто будет мешать дергать GetInterfaceForClientType* всем, кому не лень? Где гарантия того, что GetInterfaceForClientType1 будет вызван именно первым клиентом?
                                Интерфейс. Тот, у кого нет описания этого интерфейса не сможет к нему обратиться. В том то и косяк friend, что он создает очень хрупкую архитектуру. Если потом понадобится дать доступ еще одному классу, то придется вносить изменения в библиотеку.
                                Как все это будет защищать от перекомпиляций при изменении потрохов Server?
                                Вы же сами просили абстрактный пример. Внешний интерфейс Server не зависит от потрохов. Для простоты, представьте что вся реализация в Pimpl.
                                Где и как будут создаваться экземпляры InterfaceForClient?
                                Прелесть в том, что как угодно, вас как пользователя это не должно волновать.
                                Кто и как будет гарантировать время их жизни?
                                Не понял? Время жизни ссылки точно такое же как время жизни самого объекта. Его гарантирует стандарт. Вы скокнули совсем в другую область. Просили абстрактный пример, я его вам привел. Допустим копирование интерфейса запрещено, и конструктор и оператор копирования = delete.


                                1. masterspline
                                  14.04.2017 22:19

                                  > Тот, у кого нет описания этого интерфейса не сможет к нему обратиться.

                                  Описания интерфейса не будет только у того, кто не включил соответствующий заголовочный файл. Так как тут, явно должен быть отдельный заголовок с ним, который будут включать тот, кто создает интерфейс и тот кто его использует. Так что защита получается забавная: включил заголовок — можешь использовать интерфейс, не включил — не сможешь.

                                  Мне кажется, ты совсем не понял, о чем статья.


                                  1. Videoman
                                    14.04.2017 22:40
                                    -1

                                    Мне кажется это вы не понимаете в построении правильной архитектуры. Какая защита, вы о чем?! Вы часом не администратор? Кто тогда вам мешает тогда просто дописать в класс: friend class YourNewClass? В программировании основная задача, это управление растущей сложностью проекта. В правильном коде должны применяться такие конструкции, которые при развитии и сопровождении проекта потребуют как можно более локальных изменений, а не наоборот. Ваша статья решает проблему, о которой я никогда не слышал, которой просто не бывает в коде с правильно архитектурой. Friend — это «антипаттерн». Если вы создаете публичный метод, к которому принципиально имеет доступ только избранный класс, то у вас очень сильный повод задуматься над тем, что вы идете не в ту сторону и пора проводить рефакторинг кода.


                                    1. eao197
                                      14.04.2017 23:53

                                      Ваша статья решает проблему, о которой я никогда не слышал
                                      Кстати да, это многое объясняет.


                                      1. Videoman
                                        15.04.2017 00:43

                                        Что это, что многое и кому объясняет. Вы начинаете дискутировать с самим собой.


                                        1. eao197
                                          15.04.2017 08:46

                                          Что это, что многое и кому объясняет.
                                          Это — это стиль вашей аргументации, точнее ее отсутствие. Многое — это попытки предложить «лучшее» решение для проблемы, о которой вы «никогда не слышали». Кому — мне, например. Может и не только мне.
                                          Ну так и я о том же, ничем.
                                          Вы сами признаете, что решения ничем не отличаются, но при этом ваше якобы «решение» лучше. Если бы вы удосужились сделать свой пример, в котором определение интерфейсов и их реализация, а так же объединение всего этого под одной крышей была бы доведена хотя бы до уровня примера algorithmist, то вы сами могли бы увидеть, что на счет обфускации и усложения все было бы не так однозначно.
                                          Вы можете ответить на вопрос, какую реальную архитектурную проблему решает данная, так называемая, «идиома»?
                                          В моей практике было случаи, когда что-то вроде Attorney приходилось использовать. Однажды это был класс, реализованный на базе PImpl, и который был связующим звеном внутри фреймворка (т.е. разные части фреймворка имели ссылку на экземпляры этого класса). И нужно было, чтобы API этого класса был поделен на «зоны» — вот эта функциональность доступна только пользователям фреймворка, вот эта — только самому фреймворку, вот эта — плагинам, которые можно дописывать для фреймворка.

                                          Вряд ли вас такое объяснение удовлетворит, но тем не менее, на практике ситуации для Attorney и Passkey встречаются.


                                1. eao197
                                  14.04.2017 23:52

                                  Не понял?
                                  Это очевидно.
                                  А теперь перечитайте свои же ответы и попробуйте объяснить, чем ваши абстрактные интерфейсы в абстрактных примерах отличаются от механизма с классом Attorney.


                                  1. Videoman
                                    15.04.2017 00:41
                                    -1

                                    Ну так и я о том же, ничем. Так зачем же заниматься усложнением и обфускацией и так используемых всеми приемов и даже не попытаться привести пример из реальной жизни и объяснить зачем нужно так все усложнять и запутывать.
                                    Вы можете ответить на вопрос, какую реальную архитектурную проблему решает данная, так называемая, «идиома»?


                              1. ViTech
                                15.04.2017 17:19

                                Я тоже считаю, что эти Attorney-Client и Passkey — есть костыли с ненужными сущностями при кривой архитектуре. Методы, доступные из сервера для каждого типа клиентов, должны быть сгруппированы в интерфейсы, а не из кучи кишок класса вынесены наружу. Вариантов, как это можно сделать может быть несколько. Один из них и представлен выше, может его только доработать нужно. Можно попробовать сделать как-то так:

                                class Server
                                {
                                public:
                                    void provideInterfaceTo( Client1 & client )
                                    { if ( canGrantAccessTo( client ) ) client.admit( new InterfaceForClient1() ); }
                                
                                    void provideInterfaceTo( Client2 & client )
                                    { if ( canGrantAccessTo( client ) ) client.admit( new InterfaceForClient2() ); }
                                
                                    void provideInterfaceTo( Intruder & intruder )
                                    { delete &intruder; }
                                };
                                


                                Пример полностью
                                class InterfaceForClient1{};
                                class InterfaceForClient2{};
                                class InterfaceForIntruder{};
                                
                                class Client1
                                {
                                public:
                                    void admit( InterfaceForClient1 * server ) {}
                                };
                                class Client2
                                {
                                public:
                                    void admit( InterfaceForClient2 * server ) {}
                                };
                                class Intruder
                                {
                                public:
                                    void admit( InterfaceForIntruder * server ) {}
                                };
                                
                                class Server
                                {
                                public:
                                    void provideInterfaceTo( Client1 & client )
                                    { if ( canGrantAccessTo( client ) ) client.admit( new InterfaceForClient1() ); }
                                
                                    void provideInterfaceTo( Client2 & client )
                                    { if ( canGrantAccessTo( client ) ) client.admit( new InterfaceForClient2() ); }
                                
                                    void provideInterfaceTo( Intruder & intruder )
                                    { delete &intruder; }
                                
                                protected:
                                    bool canGrantAccessTo( const Client1 & client ) const { return true; }
                                    bool canGrantAccessTo( const Client2 & client ) const { return true; }
                                };
                                
                                int main()
                                {
                                    Client1 client_1;
                                    Client2 client_2;
                                    Intruder intruder;
                                
                                    Server server;
                                
                                    server.provideInterfaceTo( client_1 );
                                    server.provideInterfaceTo( client_2 );
                                    server.provideInterfaceTo( intruder );
                                
                                    return 0;
                                }
                                


                                1. qw1
                                  15.04.2017 18:04
                                  +1

                                  Это решение совсем другой задачи.

                                  Автор статьи пытается сделать проверку в compile-time и с нулевыми накладными расходами.

                                  Здесь же проверка делается в runtime (canGrantAccessTo), причём возможна эксплуатация в стиле

                                  class IntruderClient2 : public Client2 { ... };
                                  IntruderClient2 client_2;
                                  server.provideInterfaceTo( client_2 );

                                  Если делать в таком стиле, то уж с настоящей авторизацией:
                                  class Server
                                  {
                                  public:
                                      ServerAccessInterface* getServerAccess(const char* username, const char* password);
                                  }


                                  1. ViTech
                                    15.04.2017 18:28
                                    -1

                                    А если убрать canGrantAccessTo, то получится «проверка в compile-time и с нулевыми накладными расходами»? Упор в примере совсем на другое-то сделан. canGrantAccessTo — это дополнительная функциональность, при появлении хотелки разрешать доступ не только по типу класса, но и по существующему объекту. И под авторизацией не обязательно понимается проверка логина и пароля.

                                    Эксплуатаций можно много придумать, вплоть до внедрения Паблика Морозова ).


                                1. masterspline
                                  15.04.2017 18:15

                                  > Я тоже считаю, что эти Attorney-Client и Passkey — есть костыли с ненужными сущностями при кривой архитектуре.

                                  Вообще, любая нетривиальная идиома появляется, потому что тривиальное решение «не очень» и его хочется улучшить.

                                  > Методы, доступные из сервера для каждого типа клиентов, должны быть сгруппированы в интерфейсы, а не из кучи кишок класса вынесены наружу. Вариантов, как это можно сделать может быть несколько. Один из них и представлен выше, может его только доработать нужно. Можно попробовать сделать как-то так:

                                  Если из сколь угодно сложного решения выкинуть детали реализации, то в общих чертах все будет очень просто. Ты в своем решении не привел реализации интрефейсов, в результате оно выглядит простым.

                                  • Непонятно, как после вызова
                                    client.admit( new InterfaceForClient1() );
                                    

                                    клиент доберется до сервера.
                                  • Если контроль доступа делается на уровне имени класса, а не анализа в рантайме, например, состояния класса, то вызов
                                    if ( canGrantAccessTo( client ) )
                                    

                                    вообще не нужен.
                                  • Непонятно, чем это лучше Attorney, кроме наличия накладных расходов времени выполнения.


                                  Идея использовать что-то похожее на Visitor и вообще не пытаться контролировать кому даешь доступ, а предоставлять его конкретному экземпляру, использую перегрузку, интересный подход, но только если нужен контроль в runtime.


                                  1. ViTech
                                    15.04.2017 18:46

                                    Непонятно, как после вызова
                                    client.admit( new InterfaceForClient1() );
                                    

                                    клиент доберется до сервера.

                                    Это похоже на интригу ). А что клиенту от сервера нужно? Весь необходимый набор методов в этом интерфейсе и передаётся.

                                    Проверку canGrantAccessTo никто и не навязывает.

                                    Непонятно, чем это лучше Attorney, кроме наличия накладных расходов времени выполнения.

                                    Лучше тем, что нет friend, и левых сущностей, не относящихся к предметной области.


                                1. masterspline
                                  15.04.2017 18:30

                                  И еще один вопрос про

                                  client.admit( new InterfaceForClient1() );
                                  

                                  Кто и когда будет уничтожать экземпляр интерфейса? Он будет создаваться для каждого экземпляра клиента и уничтожаться при в его деструкторе или это будет разделяемый между клиентами экземпляр? Но это уже слегка offtop про memory leak и exception safety, который показывает степень «проработанности» идеи.


                                  1. ViTech
                                    15.04.2017 18:55

                                    Кто и когда будет уничтожать экземпляр интерфейса?

                                    Это уже совсем другая задача. В примере я так написал для краткости. Если сервер напрямую реализует интерфейс InterfaceForClient1 (наследуется от него), то можно передать просто this. Это если с raw-указтелями работать, чего делать совсем не стоит. И нужно мутить как минимум с умными указателями.

                                    Это вопрос из той же серии, почему в вашем примере сервер передаётся по ссылке, и есть ли уверенность, что этот сервер будет жить дольше всех его клиентов.


                                1. eao197
                                  15.04.2017 18:35
                                  +1

                                  Простите, а вы не пытаетесь в C++ привычки из Java перенести? Уверены, что new InterfaceForClient1() для предоставления доступа к некой части функциональности Server-а — это разумная цена? А то ведь подход на базе Attorney вообще накладных расходов не имеет.

                                  Ну а это просто прекрасно:

                                  void provideInterfaceTo( Intruder & intruder )
                                      { delete &intruder; }
                                  
                                  Сами придумали или подсмотрели где-то?


                                  1. ViTech
                                    15.04.2017 19:01
                                    -1

                                    Я хочу перенести привычки из хорошей архитектуры. Это лишь один из вариантов, причём набросок, а не конечное решение. Без friend и непонятных сущностей. Вдруг кто из него что-то полезное почерпнёт.

                                    Ну а это просто прекрасно:
                                    void provideInterfaceTo( Intruder & intruder )
                                        { delete &intruder; }
                                    

                                    Сами придумали или подсмотрели где-то?

                                    Я с интрудерами только так и поступаю. А Вы?


                                    1. eao197
                                      15.04.2017 19:12
                                      +1

                                      Это лишь один из вариантов, причём набросок, а не конечное решение.
                                      Ну понятно. В статье описаны давно опробированные и работающие приемы. А взамен предлагаются некие наброски с явно более высокой ценой и непонятными перспективами. И все это под абстрактными флагами «хорошей архитектуры».
                                      Без friend
                                      И откуда такая ненависть к friend? Вот таком контексте вы так же против friend-а?
                                      class some_nontrivial_class {
                                      public :
                                        ...
                                        friend void swap(some_nontrivial_class & a, some_nontrivial_class & b) {
                                          ... /* обмен содержимым */
                                        }
                                      };

                                      Я с интрудерами только так и поступаю. А Вы?
                                      А мне как-то не доводилось встречать случаев, когда бы разработчик классов, подобных Server, знал бы про существование Intruder-ов и подобных ему классов.


                                      1. ViTech
                                        15.04.2017 20:02
                                        -1

                                        Я рад, что вам понятно ). Отделять интерфейсы от реализации это я только что придумал, и перспективы этого весьма туманны.

                                        friend давно разжевали, нет необходимости ещё раз это делать. Вы лучше расскажите, как будет приятно везде этот Passkey таскать, и других убедить это делать.

                                        А мне как-то не доводилось встречать случаев, когда бы разработчик классов, подобных Server, знал бы про существование Intruder-ов и подобных ему классов.

                                        — Видишь интрудера?
                                        — Нет!
                                        — И я не вижу. А он есть!
                                        (с)


                                        1. eao197
                                          15.04.2017 22:26

                                          Ирония вместо технических аргументов и примеров кода — это отличный способ завершить спор. Тем более, что вот эти слова:

                                          как будет приятно везде этот Passkey таскать, и других убедить это делать
                                          явно демонстрируют уровень понимания сути проблемы.


                                        1. qw1
                                          16.04.2017 09:13

                                          Вы лучше расскажите, как будет приятно везде этот Passkey таскать
                                          Куда таскать? Можно объявить параметр со значением по умолчению
                                          void some_method( Passkey p = Passkey())
                                          и тогда на клиенте исчезнет синтаксический оверхед, можно вызывать функцию, не упоминая о паскее. А компилятор соптимизирует передачу объекта длины 0 в пустой код.


                                          1. ViTech
                                            16.04.2017 09:59

                                            Можно объявить параметр со значением по умолчению
                                            void some_method( Passkey p = Passkey())
                                            


                                            Полнее код пишите, пожалуйста. В каких классах какие методы будут с таким параметром по умолчанию?


                                            1. qw1
                                              16.04.2017 13:19

                                              Ниже полностью работающий пример, на основе авторского

                                              Скрытый текст
                                              class Server
                                              {
                                              public:
                                                  class Passkey
                                                  {
                                                  private:
                                                      friend class Client;
                                                      friend class Server;
                                                      Passkey() noexcept {}
                                                      Passkey( Passkey&& ) {}
                                                  };
                                                  void some_method( Passkey p = Passkey())
                                                  {
                                                      some_method_impl();
                                                  }
                                              
                                              private:
                                                  void some_method_impl();
                                                  void one_more_method();
                                              };
                                              
                                              class Client
                                              {
                                              private:
                                                  void do_something( Server& server );
                                              };
                                              
                                              void Client::do_something( Server& server )
                                              {
                                                  server.some_method( Server::Passkey() ); // авторский способ вызова
                                                  server.some_method(); // предложенный способ вызова
                                              }


                                          1. masterspline
                                            16.04.2017 14:24
                                            +1

                                            void some_method( Passkey p = Passkey())
                                            

                                            Так делать нельзя, я об этом написал в статье (сам думал, что можно, но в таком случае этот метод сможет вызвать кто угодно).
                                            Пример, на основе кода ниже
                                            class Server
                                            {
                                            public:
                                                class Passkey
                                                {
                                                private:
                                                    friend class Client;
                                                    friend class Server;
                                                    Passkey() noexcept {}
                                                    Passkey( Passkey&& ) {}
                                                };
                                                void some_method( Passkey p = Passkey())
                                                {
                                                    some_method_impl();
                                                }
                                            
                                            private:
                                                void some_method_impl();
                                                void one_more_method();
                                            };
                                            
                                            class Client
                                            {
                                            private:
                                                void do_something( Server& server );
                                            };
                                            
                                            void Client::do_something( Server& server )
                                            {
                                                server.some_method( Server::Passkey() ); // авторский способ вызова
                                                server.some_method(); // предложенный способ вызова
                                            }
                                            
                                            int main()
                                            {
                                                Server s;
                                                s.some_method(); // <- это не должно работать
                                            }
                                            


                                            1. qw1
                                              16.04.2017 16:11

                                              да, действительно


                                    1. qw1
                                      16.04.2017 09:07

                                      Я с интрудерами только так и поступаю. А Вы?

                                      Напомнило
                                      #define true false // счастливой отладки, $%#!

                                      Причём в Release-сборке delete может упасть, а может по-тихому испортить память. Но с коллегами по проекту, не умеющими использовать ваш класс, только так и надо.


                                      1. ViTech
                                        16.04.2017 10:03

                                        Но с коллегами по проекту, не умеющими использовать ваш класс, только так и надо.


                                        Об этом и речь: что и от кого защищаем?
                                        class Intruder
                                        {
                                        private:
                                            void do_some_evil_staff( Server& server )
                                            {
                                                delete &server;
                                            }
                                        };
                                        

                                        Достаточно злодейское действие?


                                        1. qw1
                                          16.04.2017 13:27

                                          Об этом и речь: что и от кого защищаем?

                                          Я думаю, тут такой сценарий. Архитектор или Senior решили, что с Server взаимодействуют только определённые классы, и это ограничение явно прописали в код Server.

                                          Какой-нибудь Junior может, забыв о договорённостях, напрямую вызывать Server из своего класса (условно, Intruder). Причём изменения в Server тщательно ревьюятся, чтобы не допустить туда добавления новых friends, а изменения всех Intruders (читай — говноклассов бизнес-логики) проверять лень.

                                          Нужно, чтобы пролезть в сервер было максимально сложно, не меняя его исходников.


  1. Algoritmist
    14.04.2017 17:28
    +1

    Через интерфейсы будет удобнее.
    Вообще, считаю появление friend — поводом задуматься об архитектуре.
    Если вернуться к статье, то последний вариант (Passkey)

    можно написать просто так:
    // === server.hpp
    class Client;
    class SuperClient;
    
    class Server
    {
    public:
        void proxy_some_method( Client& ); // proxy для Client
        void proxy_some_method( SuperClient& ); // proxy для SuperClient
    
    private: // закрытый интерфейс
        void some_method(); // метод для Client
        void one_more_method(); // этот метод должен остаться закрытым
    private:
        // далее детали реализации класса...
    };
    
    inline void Server::proxy_some_method( Client& )
    {
        some_method();
    }
    
    inline void Server::proxy_some_method( 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( *this );
    }
    
    // evil.hpp
    class Intruder
    {
    private:
        void do_some_evil_staff( Server& server )
        {
            // server.some_method(); // <- это не сработает
            // server.proxy_some_method( *this ); // и это тоже
            // server.proxy_some_method( Client() ); // можно сломать так, но зачем...
        }
    };
    


    1. Algoritmist
      14.04.2017 17:45

      Согласен, мой пример хуже Passkey.


    1. masterspline
      14.04.2017 18:33

      > Через интерфейсы будет удобнее.

      Приведи пример кода, пожалуйста. Чтобы сравнить с описанными вариантами. По моим понятиям, в первом варианте class Attorney — вполне себе интерфейс. Но, думаю, ты под интерфейсом понимаешь что-то другое.

      > Вообще, считаю появление friend — поводом задуматься об архитектуре.

      Одна из мыслей, которые меня преследуют с появления идеи написания статьи, что архитектуру моего приложения можно сделать лучше. И одна из целей статьи — улучшить знания и навыки проектирования приложений (это ж не корпоративный блог, а написать хорошую статью не так просто, кто пробовал — знает).


      1. Algoritmist
        14.04.2017 20:13

        Да, у меня без friend не выходит (и для этой задачи, похоже не выйдет).
        Под вариантом с «интерфейсом» я понимал

        такой код:
        // === interface.hpp
        class Client;
        class SuperClient;
        
        class Server_InterfaceForClient
        {
        protected:
            friend Client;
            friend SuperClient;
            Server_InterfaceForClient() noexcept {}
            Server_InterfaceForClient( Server_InterfaceForClient&& ) {}
            virtual void some_method_for_client() = 0;
        private:
            Server_InterfaceForClient( const Server_InterfaceForClient& ) = delete;
            Server_InterfaceForClient& operator=( const Server_InterfaceForClient& ) = delete;
            Server_InterfaceForClient& operator=( Server_InterfaceForClient&& )      = delete;
        };
        
        // === server.hpp (этот файл прячем от посторонних глаз)
        #include "interface.hpp"
        
        class Server : public Server_InterfaceForClient
        {
        protected:
            virtual void some_method_for_client( ) {some_method();} // proxy для Client
        
        private: // закрытый интерфейс
            void some_method(); // метод для Client
            void one_more_method(); // этот метод должен остаться закрытым
        private:
            // далее детали реализации класса...
        };
        
        // === client.hpp
        #include "interface.hpp"
        
        class Client
        {
        private:
            void do_something( Server_InterfaceForClient& server );
        };
        
        void Client::do_something( Server_InterfaceForClient& server )
        {
            // server.some_method(); // <- так не сработает
            // server.one_more_method(); // <- этот метод тоже не доступен
            server.some_method_for_client( );
        }
        
        // === evil.hpp
        #include "interface.hpp"
        
        class Intruder
        {
        private:
            void do_some_evil_staff( Server_InterfaceForClient& server )
            {
                // server.some_method(); // <- это не сработает
                // server.some_method_for_client( ); // и это тоже
            }
        };
        


      1. Door
        16.04.2017 20:42

        По поводу интерфейсов — тут уже говорили. И мне тоже кажется, что они здесь более уместны.
        С интерфейсами — будет проще и понятней разобраться новичку, они более четко документируют код. И, что тоже важно, уменьшают количество "дополнительных абстракций", в которых тоже приходится разбираться, читая код.


        Что я имею в виду, так это такой код:


        struct IClientAccess
        {
            virtual int client_access() = 0;
        
        protected:
            ~IClientAccess() = default;
        };
        
        struct IIntruderAccess
        {
            virtual int intruder_access() = 0;
        
        protected:
            ~IIntruderAccess() = default;
        };
        
        struct Server final :
            public IClientAccess,
            public IIntruderAccess
        {
            virtual int client_access() override
            {
                return 1;
            }
        
            virtual int intruder_access() override
            {
                return 2;
            }
        };
        
        struct Client
        {
            IClientAccess& access;
        
            Client(IClientAccess& access)
                : access{access}
            {
            }
        
            int do_work()
            {
                return access.client_access();
            }
        };
        
        struct Intruder
        {
            IIntruderAccess& access;
        
            Intruder(IIntruderAccess& access)
                : access{access}
            {
            }
        
            int do_work()
            {
                return access.intruder_access();
            }
        };
        
        int main()
        {
            Server s;
            Client c{s};
            Intruder i{s};
            return c.do_work() + i.do_work();
        }
        

        Фишка в том, что код до безобразия простой и понятный, а для тех, кто волнуется по поводу рантайма — есть несколько аргументов.


        • Девиртуализация — мы помогли компилятору как могли (например, пометили Server как final) и он может убрать виртуальный вызов.

        clang -O3:


        main:                                   # @main
                mov     eax, 3
                ret
        

        gcc -O3:


        main:
                mov     eax, 3
                ret
        

        и… Visual C++ не справился


        Как видите — кое-кто смог убрать виртуальные колы (и это не потому, что ф-и очень простые — если они не заинлайнятся — так всё равно будет прямой вызов к экземпляру Server… я надеюсь)


        • В случае, если производительность приложения упала и профилировщики показывают, что виновник — это именно эти виртуальные вызовы… кхм. Думаю, что стоит подумать о архитектуре ещё раз и уже тогда добавлять какие-то прослойки и абстракции, если выхода нет.


        1. masterspline
          16.04.2017 21:19

          Подобный пример мне уже приводили еще вчера вечером. Я уже тогда был уверен, что виртуальные вызовы в Server будут оптимизированы, если класс объявить final. В этом решении суть в том, что оно не решает поставленную задачу. Intruder вполне может сделать привидение (cast) к IClientAccess и получить доступ клиента.

          Думаю причина большого числа возражений в том, что:

          • Народ просто не заморачивался на такой задаче или вообще не сталкивался.
          • Решение очень непривычное.
          • Пример подобран не удачно

          Могу предложить такую задачу: есть класс Server (или с другим именем), который как-то обрабатывает данные. Данные на обработку можно отправлять через открытый интерфейс. У него внутри есть стратегия обработки данных, которую можно менять, но только если вызов на замену приходит от определенного класса (Manager). Таким образом, я не хочу, чтобы любой клиент сервера мог сменить стратегию расчета. Или, например, у меня есть вызов, который ставит данные на обработку с более высоким приоритетом, но он должен быть доступен только избранным.
          Пример:
          class CalculatorMK_52
          {
          public:
              void set_reg( unsigned arg ) { reg = arg; };
              unsigned add(unsigned arg) { return arg + reg; }
              unsigned mul(unsigned arg ) { return arg * reg; }
          private:
              // этот метод крайне ресурсоемкий (20-40 тактов процессора Intel),
              // его можно выполнять только при вызове из приоритетного клиента
              // к тому же при делении на ноль не факт что вылетит исключение, возможно будет terminate()
              unsigned div(unsigned arg) { return arg1 / reg; }
          
              unsigned reg = 0;
           };
          

          Такой вот класс, рефакторить тут особо нечего. Нужно ограничить доступ к div() только избранным классам, а в детали реализации вообще никто лезть не должен. По сути div() часть интерфейса класса, но к ней нужно ограничить доступ.


          1. ViTech
            17.04.2017 11:59

            Пример подобран не удачно

            Поэтому хотелось бы увидеть пример, аналогичный реально используемому. Сколько у вас, в среднем, у классов типа Server таких вот friend'ов? Клиенты могут хранить связь с сервером (ссылка, указатель разной степени умности)? Как эта идиома чувствует себя в многопоточной среде?

            Народ просто не заморачивался на такой задаче или вообще не сталкивался.

            Проблема с разграничением доступа к объекту появилась совсем недавно, в начале 2017 года? Раньше таких задач не возникало? Лично я как-то не очень наблюдаю повсеместного использования Passkee. А вы?


            1. eao197
              17.04.2017 12:11
              +1

              Как эта идиома чувствует себя в многопоточной среде?
              А какое отношение многопоточность имеет к видимости внутренностей классов?
              Лично я как-то не очень наблюдаю повсеместного использования Passkee.
              Его повсеместное применение и не нужно. Это частное решение для очень узкого круга задач, когда другие, более очевидные подходы (например использование интерфейсов в стиле языка Java) не работают или есть какие-то противопоказания к их применению.


              1. ViTech
                17.04.2017 12:24
                -2

                А какое отношение многопоточность имеет к видимости внутренностей классов?

                Тут начали такты считать на вызов виртуальных методов, вот мне и стало интересно, насколько хорошо решены накладные расходы на многопоточность.
                Это частное решение для очень узкого круга задач...

                Поэтому и хотелось бы увидеть в статье более подробное описание области, в которой предложенная идиома может хорошо себя показать (кроме того, что она такая, красивая, в принципе есть). Лучше расписать её достоинства, недостатки и ограничения. В идеале ещё привести сравнение с альтернативными решениями.


                1. eao197
                  17.04.2017 12:31

                  Тут начали такты считать на вызов виртуальных методов, вот мне и стало интересно, насколько хорошо решены накладные расходы на многопоточность.
                  К многопоточности эта идиома вообще не имеет никакого отношения. А такты начали считать поскольку для решения таких же проблем любители «красивой архитектуры» предлагают задействовать «интерфейсы в стиле Java», что влечет за собой использование виртуальных методов там, где Passkey и Attorney позволяют обходиться прямыми вызовами (ну или вызовами с меньшей косвенностью, если речь идет о PImpl).
                  Лучше расписать её достоинства, недостатки и ограничения.
                  Думаю, у автора была задача просто рассказать о наличии таких идиом. Ибо если идиому Attorney многие переизобретают даже не подозревая об этом, то вот Passkey более хитрая штука и не зная про ее изобрести ее «с нуля» смогут далеко не все. А анализ и сравнение — это уже тема для отдельной статьи.


                  1. Door
                    17.04.2017 14:07

                    любители «красивой архитектуры» предлагают задействовать «интерфейсы в стиле Java», что влечет за собой использование виртуальных методов там, где Passkey и Attorney позволяют обходиться прямыми вызовами

                    ну вы как-то слишком с "любителями красивой архитектуры". С Passkey и Attorney публичный интерфейс Server-а обрастает ненужными знаниями: почему я должен при добавлении нового клиента идти в Server.h и добавлять forward declaration для нового клиента? Как-то нелогично, что Server знает почти всех своих пользователей. В конце-концов, если утрировать, вы же не идёте в класс std::basic_string<> и не добавляете обьявление своего класса/не важно чего, который хочет использовать строки?
                    Вот, комментарий ниже — там уже идёт попытка избавиться от дублирования и прочее при появлении нового клиента.
                    Получается так, что всё хорошо, если мы будем придерживаться очень специфичных входных условий "количество клиентов — 1 или 2, есть 2 ф-и, нужно распределить доступ к ним между этими клиентами"


                    (на заметку — мне лично решение симпатизирует. Просто идёт рассуждение про применимость этой идиомы для других случаев с кучей вопросов "а что если")


                    1. eao197
                      17.04.2017 14:11
                      +1

                      С Passkey и Attorney публичный интерфейс Server-а обрастает ненужными знаниями: почему я должен при добавлении нового клиента идти в Server.h и добавлять forward declaration для нового клиента?
                      С Passkey так и есть. В случае с Attorney не так это очевидно.
                      Просто идёт рассуждение про применимость этой идиомы для других случаев с кучей вопросов «а что если»
                      У меня сложилось ощущение, что у многих комментаторов здесь нет понимания, что Passkey и Attorney — это эдакий «лом», который стоит применять там, где остальные, более удобные, простые и безопасные идиомы не подошли по тем или иным причинам. Но уж в этих случаях по эффективности и другим параметрам альтернативу Passkey и Attorney подобрать не так просто.


                      1. Door
                        17.04.2017 14:34

                        угу. К примеру, у Eiffel есть возможность указывать список классов, которым доступна ф-я:


                        feature {DECIMAL, DCM_MA_DECIMAL_PARSER, DCM_MA_DECIMAL_HANDLER} -- Access

                        Here, the compiler will allow only the classes listed between the curly braces to access the features within the feature group (e.g. DECIMAL, DCM_MA_DECIMAL_PARSER, DCM_MA_DECIMAL_HANDLER).

                        В этом случае, Passkey и Attorney — просто попытка добиться того же. Как и почти всегда — в случае с C++ — круто то, что язык позволяет сделать тебе такой воркэраунд — как следствие — воркэраунд он на то и воркэраунд — вызывает много вопросов.
                        Лично я не фанат возможности указывать "именно список классов, которым разрешается доступ к фиче". Возможно поэтому и спорю :)


                      1. ViTech
                        17.04.2017 14:36
                        -1

                        У меня сложилось ощущение, что у многих комментаторов здесь нет понимания, что ....

                        Как вы думаете, в чём причина того, что комментаторы это не понимают? В том что они не очень умные, или в том, что в статье об этом мало написано? Для меня классы из статьи выглядят публичными классами какой-то библиотеки, а не деталями её реализации. По-хорошему, детали реализации не должны быть доступны пользователям публичных классов и пишутся обычно небольшой группой разработчиков. Они не доверяют друг другу?


                        1. eao197
                          17.04.2017 14:46
                          +2

                          Как вы думаете, в чём причина того, что комментаторы это не понимают?
                          Есть ощущение, что это просто от недостатка опыта работы именно с C++ и из-за попыток бездумно перенести в С++ опыт из языков, с поддержкой меньшего количества парадигм (в частности из Java и C#).
                          Для меня классы из статьи выглядят публичными классами какой-то библиотеки, а не деталями её реализации.
                          Это ваше личное мнение, сложно судить, почему оно у вас именно такое.
                          Они не доверяют друг другу?
                          Я бы смотрел на это таким образом:

                          * может быть ситуация, когда разрабатывается большой фреймворк, состоящий из нескольких модулей;
                          * в одном из основных модулей определяется некий класс A, который должен быть доступен всем (как остальным модулям фреймворка, так и пользователям фреймворка);
                          * части модулей фреймворка от класса A может потребоваться какая-то часть функциональности, объявленная в private-секции класса. Поскольку мы находимся внутри фреймворка и, в принципе, понимаем что делаем, то такой доступ можно дать. Но только части модулей самого фреймворка. Пользователи фреймворка доступа к этой функциональности иметь не должны.

                          Идиомы Attorney и Passkey вполне уместны здесь. А в реализации могут быть более удобными и дешевыми, чем какие-то другие схемы. При этом, в принципе, разные модули фреймворка могут разрабатываться разными людьми, для разных задач с разными требованиями. Поэтому может быть разный набор Attorney/Passkey.


                          1. ViTech
                            17.04.2017 15:46

                            Это ваше личное мнение, сложно судить, почему оно у вас именно такое.

                            То же самое можно сказать и про ваше личное мнение, что в статье автор ведёт речь про детали реализации, а не про публичные классы. На чём основана такая уверенность? В статье есть какие-нибудь предпосылки к такому мнению?

                            Читатель с «недостатком опыта работы именно с C++» увидит эту статью, ему понравятся эти решения и он решит применить их в своих проектах. Вы уверены, что он со своим уровнем опыта применит их правильно, в деталях реализации, а не потащит в публичную часть? За опытных разработчиков я не переживаю ).

                            Я бы смотрел на это таким образом:
                            ...

                            Это хорошо, что вы объясняете. Странно, что не автор это делает. Читателям статьи, чтобы уточнить существенные детали по применимости этой идиомы, теперь придётся извлекать их из груды комментариев.


                            1. eao197
                              17.04.2017 15:53

                              На чём основана такая уверенность?
                              Простите, уверенность в чем?

                              После прочтения статьи у вас сложилось ваше впечатление. Я не знаю, почему оно у вас сложилось именно таким. Может на основании вашего опыта. Может вы не выспались. Может перед этой статьей читали какую-то другую статью и одно на другое наложилось. Я не могу сказать, почему вы восприняли статьи именно так.

                              Исходя из своего опыта я просто более-менее представляю, где оба описанных подхода могут использоваться. И для меня эта статья полезна именно тем, что в ней коротко описываются детали каждой из идиом без лишних объяснений зачем и для чего нужны сами идиомы. Такой нормальный справочный материал.
                              Странно, что не автор это делает.
                              Может у человека просто времени нет на то, чтобы написать хороший обзор нескольких подходов и сравнительный анализ их применимости, выгод и недостатков.


                              1. ViTech
                                17.04.2017 16:16

                                Простите, уверенность в чем?

                                В том:
                                что в статье автор ведёт речь про детали реализации, а не про публичные классы


                                Как я уже сказал, за опытных разработчиков я не переживаю. Я не уверен, что остальные читатели столь же опытны, как вы, и не хотел бы чтобы они применяли предлагаемые решения неправильно. Пусть они об этом хотя бы из комментариев узнают.


                                1. eao197
                                  17.04.2017 16:37

                                  Но ведь это ваша фраза. Я такого не говорил, соответственно, мне не понять, откуда у меня может быть уверенность в том, о чем я не говорил.


                                  1. ViTech
                                    17.04.2017 16:52

                                    Хорошо. Тогда расскажите, откуда у вас появилось

                                    ощущение, что у многих комментаторов здесь нет понимания, что Passkey и Attorney — это эдакий «лом», который стоит применять там, где остальные, более удобные, простые и безопасные идиомы не подошли по тем или иным причинам.

                                    и что автор именно «эдакий лом» в статье и подразумевал. И только неопытные комментаторы об этом не догадались и не поняли.


                                    1. eao197
                                      17.04.2017 16:59

                                      Так здесь же много правильных слов сказали о том, что friend — это нарушение инкапсуляции. Где-то это нарушение оправдано (например, когда определяется friend void swap), но в большинстве случаев friend — это маркер, который означает, что другого, более безопасного решения не нашлось. Тому может быть несколько причин (навскидку):

                                      * у человека просто не было опыта и он влепил friend не понимая, чем это может горозить в исторической перспективе;
                                      * решение с friend является quick-and-dirty временным решением, которое сделали ибо не было возможности выполнить нормальный рефакторинг;
                                      * решение с friend объективно лучше по совокупности каких-то параметров и оно оправдано в данном контексте.

                                      Поскольку описание того же Passkey мне доводилось видеть и раньше, а так же доводилось применять Attorney несколько раз, то у меня уже было представление о ситуациях, в которых такие идиомы оправданы. И это именно ситуации, когда нужно что-то из категории «против лома есть приема».

                                      В статье об этом не говориться. Но как я уже сказал, для меня статья ценна тем, что она больше похоже на справочный материал, а не на учебник для новичков.


                      1. Videoman
                        17.04.2017 16:25
                        -1

                        А у меня складывается ощущение, что у вас мания величия и кроме вас С++ больше никто не знает (хотя от части так и есть).
                        Основные претензии к методам показанным в статье:
                        1. То что это называется «идиома» — т.е. новички в С++ могут подумать что это устоявшиеся полезные практики и начать это использовать повсеместно. Хотя, запихивание разных не связанных интерфейсов в один объект — это «антипаттерн»
                        2. То, что данные подходы, якобы, не предоставляют доступа к деталям реализации — сколько раз уже повторялось, что private секция является очень слабым способом изоляции, почти нулевым. Все что там находится, все участвует в разрешении имен, перегрузках, тащит за собой инклюды и засоряет пространство имен кода использующих класс.

                        Вы так и не смогли объяснить, зачем так писать код, чтобы нужно было потом использовать такие финты. За 17 лет коммерческой разработки на С++ у меня ни разу не было ситуации, когда «God Object» нельзя было бы разделить на меньшие, каждый со своим интерфейсом.

                        «RAII» — идиома
                        «Copy and Swap» — идиома
                        «Non-virtual interface» — идиома
                        По каждой я могу объяснить как она работает и какие проблемы решает, а то что приведено в статье — это вредный финт ушами, так как к сторонней библиотеке это не применишь, а свой код так не напишешь.


                        1. eao197
                          17.04.2017 16:34
                          +1

                          А у меня складывается ощущение
                          Продолжайте держать нас в курсе, тут ресурс такой — для публикации ваших ощущений.
                          То что это называется «идиома» — т.е. новички в С++ могут подумать что это устоявшиеся полезные практики и начать это использовать повсеместно.
                          По вашим словам, «Non-virtual interface» — это тоже идиома. И что, новички должны брать и использовать ее повсеместно?
                          Вы так и не смогли объяснить, зачем так писать код, чтобы нужно было потом использовать такие финты.
                          А что, обещал? Мне приходилось использовать что-то вроде Attorney. Примеры вам приводились. То, что вас они не устраивают, ну это же не мои проблемы.
                          За 17 лет коммерческой разработки на С++ у меня ни разу не было ситуации
                          Т.е. эти 17 лет являются мерилом, которое позволяет вам глаголить абсолютную истину? Прекрасно. Не понятно, правда, почему вы тогда на простые вопросы внятно и с примерами кода ответить не можете.

                          Ну и да, 17-тью годами вы не того человека напугать решили, спрячьте линейку, такой длиной хвататься неприлично.


                          1. Videoman
                            17.04.2017 17:00

                            Очень быстро отвечаете, явно не успеваете подумать, а зря, хорошему программисту всегда лучше сначала подумать, а потом уже писать.

                            Продолжайте держать нас в курсе, тут ресурс такой — для публикации ваших ощущений.
                            По-моему, здесь ресурс для обмена мнениями, разве нет..
                            По вашим словам, «Non-virtual interface» — это тоже идиома. И что, новички должны брать и использовать ее повсеместно?
                            могут, нет проблем.
                            А что, обещал? Мне приходилось использовать что-то вроде Attorney. Примеры вам приводились. То, что вас они не устраивают, ну это же не мои проблемы.
                            «Зачем так писать ?» -здесь вопрос риторический. Не нужно распространять плохие практики. Видно, что вам никогда не приходилось пытаться поддерживать такой ужас на friend-ах — вроде классы есть, а на самом деле один большой класс, который не расширить, не изолировать.

                            Дальше пошло уже хамство. Я так, понимаю что вы человек, которые не терпит точку зрения отличную от вашей, так что желаю вам успехов. Может лет через 17 вы поймете что я хотел до вас донести.


                            1. eao197
                              17.04.2017 17:07
                              +1

                              Не нужно распространять плохие практики.
                              Вы бы сначала сделали труд доказать, что это плохие практики. Особенно когда их применяют по месту.

                              Только для этого нужно взять и показать хотя бы одно нормальное альтернативное решение. После чего провести сравнение по каким-то параметрам. Это труд, который вы на себя не берете.

                              Вот тут показали возможное решение. Как-то по объему и сложности оно не шибко выигрывает. Об этом вам здесь и говорят столько времени. Но вместо того, чтобы прислушаться, вы пытаетесь аппелировать к своему опыту (и явно нарываетесь не на тех), а потом обвиняете собеседника в хамстве. Ведите разговор конструктивно, отношение к вашим словам будет соответствующим.


                        1. eao197
                          17.04.2017 16:51
                          +1

                          у меня ни разу не было ситуации, когда «God Object» нельзя было бы разделить на меньшие, каждый со своим интерфейсом.
                          Ну и еще одна ваша проблема состоит в том, что вы ищете ответ на вопрос «можно или нельзя?». Никто не говорил, что проблему Attorney или Passkey нельзя решить каким-то другим способом. Речь о том, насколько выгодно брать тот или другой способ. Поэтому вопрос, на который стоит искать ответ, должен выглядеть как «выгодно или невыгодно?»

                          Собственно, поэтому я и хотел получить от вас вменяемые ответы на которых можно было бы уже рассуждать, что выгодно, что нет, когда и где. Вместо этого вы пытаетесь аппелируя к своему 17-тилетнему опыту заявлять, что friend плохо и private ни от чего не защищает и т.д., и т.п. Что к делу имеет косвенное отношение.


        1. qw1
          17.04.2017 13:03

          и… Visual C++ не справился
          Как видите — кое-кто смог убрать виртуальные колы
          Там весь код вообще выглядит, как собранный для DEBUG (сохранение регистровых параметров в фрейме стека при входе в функцию, например).
          А в выводе компилятора указана нераспознанная опция:

          Microsoft (R) C/C++ Optimizing Compiler Version 19.10.25017 for x64
          Copyright (C) Microsoft Corporation. All rights reserved.
          cl : Command line warning D9002 : ignoring unknown option '/O3'


          У MS Visual C++ нет /O3
          Скрытый текст
                                   C/C++ COMPILER OPTIONS
          
          
                                        -OPTIMIZATION-
          
          /O1 minimize space                      /O2 maximize speed
          /Ob<n> inline expansion (default n=0)   /Od disable optimizations (default)
          /Og enable global optimization          /Oi[-] enable intrinsic functions
          /Os favor code space                    /Ot favor code speed
          /Ox maximum optimizations               /Oy[-] enable frame pointer omission
          /favor:<blend|ATOM> select processor to optimize for, one of:
              blend - a combination of optimizations for several different x86 processors
              ATOM - Intel(R) Atom(TM) processors


          1. Door
            17.04.2017 13:32

            Да, конечно. Это я игрался с опциями и компиляторами. Перепробовал для cl разные варианты (/Ox, /O2 и т.д.), чтобы избавиться от виртуал кола. В итоге, ничего не получилось, я переключился на другие компиляторы, а ссылку для cl — уже в конце вставил, чтобы было справедливо, что не все смогли.


            1. qw1
              17.04.2017 13:55

              Поигрался сам с компилятором. И в x86, и в x64 получается интересная штука:
              первый вызов не виртуальный (по адресу функции int client_access()), второй вызов виртуальный, затем сложение.

              Получается, MSVC при использовании множественного наследования оптимизирует виртуальные вызовы только из первого предка.


              1. Door
                17.04.2017 14:35

                Интересно. Спасибо за заметку


  1. Videoman
    14.04.2017 17:47

    Согласен насчет friend-a. Вообще friend обеспечивает самую слабую степень инкапсуляции и является костылем для случаев, когда у вас нет другого выхода. Например, в случае каких-то сторонних библиотек или legacy.

    Algoritmist А вам не кажется что у вас просто Visitor получился?


    1. Algoritmist
      14.04.2017 17:55

      Да, что-то погорячился…


  1. ViTech
    15.04.2017 20:07

    Что насчёт наследования от Client? Могут ли наследники Client обращаться к серверу на правах своего предка? В постановке задачи что об этом говорится?


    1. qw1
      16.04.2017 09:15

      Не могут
      friend не наследуется.


      1. ViTech
        16.04.2017 10:22
        -1

        Это в текущей реализации так получается. А я про постановку задачи спрашивал.


        1. qw1
          16.04.2017 13:34

          Если разрешить доступ всем потомкам, получается слишком очевидная дыра: наследуешься от Client и дописываешь метод (можно static, чтобы экземпляр не создавать), в котором делаешь, что хочешь.


          1. ViTech
            16.04.2017 18:54

            А если не разрешать доступ потомкам, то интерфейсы типа AbstractClient в виде абстрактных классов (если такие вдруг понадобятся) оказываются в пролёте, я правильно понимаю? Поэтому и хотелось бы уточнить область применения этой идиомы. Если это не светится в публичной части, а используется где-то глубоко в дебрях реализации, стоит ли такой огород там городить?


            1. qw1
              16.04.2017 19:54
              +1

              А если не разрешать доступ потомкам, то интерфейсы типа AbstractClient в виде абстрактных классов (если такие вдруг понадобятся) оказываются в пролёте, я правильно понимаю?

              Неправильно понимаете. Пользователь интерфейса AbstractClient вообще не знает, какие отношения с конкретным Server у данного ему AbstractClient, это не его поле интересов.
              Если это не светится в публичной части, а используется где-то глубоко в дебрях реализации, стоит ли такой огород там городить?
              Писал выше — если header-файл сервера доступен (а он может быть нужен, т.к. от него зависит Client), то любой неосторожный разработчик напрямую может залезть в сервер, наплевав на рекомендации по архитектуре, что нежелательно.

              В принципе, pImpl решает этот вопрос, но без zero-cost, и с другими проблемами.


  1. nikabc555
    17.04.2017 09:13

    Очень интересный метод с использованием Passkey
    Но в вашей реализации мне не нравится, что proxy_some_method копипастится для каждого из клиентов. Так можно и ошибок наплодить, и при смене сигнатуры придется править каждую копию proxy_some_method

    По-моему, лучше предоставить компилятору клонирование proxy с помощью шаблонов. А выбор классов для доступа организовать с помощью type_traits и static_assert
    В итоге получится подобное:

    код C++
    #include <iostream>
    #include <type_traits>
    
    template<typename T, typename P>
    class Access {
      friend T;
      Access() noexcept {}
      Access(const Access&) = delete;
      Access(Access&&) noexcept {}
      void operator=(const Access&) = delete;
      void operator=(Access&&) = delete;
    };
    
    class Client1;
    class Client2;
    class Server
    {
      void some_method(int i) {
        std::cout << i << std::endl;
      }
    
    public:
      template<typename T>
      using AccessorSomeMethod = Access<T, Server>;
    
      template<typename T>
      inline void proxy_some_method(AccessorSomeMethod<T> ac, int i) {
        static_assert(
          std::is_same<T, Client1>::value
          || std::is_same<T, Client2>::value //если закомментировать эту строку, то будет ошибка при компиляции Client2
          , "forbidden access for this type"
        );
        some_method(i);
      }
    };
    
    class Client1
    {
    public:
      void call_some_method(Server& s)
      {
        s.proxy_some_method(Server::AccessorSomeMethod<Client1>(), 1);
      }
    };
    
    class Client2
    {
    public:
      void call_some_method(Server& s)
      {
        s.proxy_some_method(Server::AccessorSomeMethod<Client2>(), 2);
      }
    };
    
    int main()
    {
      Server server;
      Client1 client1;
      Client2 client2;
    
      client1.call_some_method(server);
      client2.call_some_method(server);
    
      return 0;
    }
    



    1. masterspline
      17.04.2017 20:53

      Я пытался сделать, чтобы в Passkey<> можно было передавать несколько параметров

      Passkey<Client1, Client2>
      

      Но не осилил, чтобы из variadic template можно было сгенерировать список friend
      friend Client1;
      friend Client2;
      ...
      

      Чтобы соответствующий Passkey<> могли создать любой из перечисленных классов. Тогда можно было бы создавать один
      void proxy_some_method(Passkey<Client1, Client2>, int i);
      

      и вызывать его
      obj->proxy_some_method({}, int i);
      

      Хотя вполне можно создать passkey.hpp с 1024 вариантами шаблона Passkey<> для 1...1024 шаблонных параметров.


  1. iCpu
    17.04.2017 10:13
    -1

    Я правильно понимаю, что это просто извращённый вид старой доброй условной компиляции? Если да, то чем ваше решение лучше небольшого шаблона?
    https://ideone.com/9pQayU
    https://godbolt.org/g/AED1wJ


  1. ViTech
    17.04.2017 12:14

    У меня ещё такие вопросы возникают:

    1. Как разработчики классов типа Client_1, Client_2, Manager, Vip и им подобных узнают, какие методы класса типа Server им доступны и разрешены, а какие запрещены? Классы Server очень хорошо задокументированы?
    2. В случае неопределённости из вопроса 1 существует довольно высокая вероятность, что разработчик таки вызовет метод Server'а, который для него запрещён. В этом случае, куда его приведёт разбор ошибок компиляции? Не в те ли потроха Server'а, которые от него так тщательно оберегаются?