Итак задача такая: есть классы 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. Нужно ли городить весь этот огород в Вашем проекте или оно того не стоит – это уже решать Вам.
Videoman
Во всех подобных случаях, гораздо полезней не запрещать, а скрывать реализацию.
Оба «паттерна», на самом деле ничего не скрывают. Оба клиента, по-прежнему, видят все «кишки» и зависят друг от друга. А нужно, и всего-то, просто предоставить два интерфейса: InterfaceForClient1, и InterfaceForClient2. В этом случае вы действительно решите поставленную цель.
eao197
Videoman
Так если ничем не отличается, зачем плодить новые сущности без необходимости. Про бритву Оккама слышали.
eao197
Videoman
Attorney у вас функционально не отличается, но вот Server-у это ничем не поможет, к сожалению. У вас там по прежнему будут методы и для Client1 и для Client2 и все их параметры (и может быть десятки типов этих параметров). Ну т.е. никакого сокрытия реализации не получается в таком случае, меняется Server и «поехали» все перекомпилировать по новой…
eao197
С чего бы пользователь класса Attorney должен был бы видеть все потроха класса Server? Все может быть вообще вот в таком виде:
Все детали реализации Attorney и Server могут быть упрятаны в *.cpp-файлы, которые клиенту не видны.Videoman
А как вот этот тогда вызов сделать:
???Если у вас Server вообще не виден, тогда это интерфейс и у вас вызов интерфейса через интерфейс. И зачем тогда так код запутывать???
eao197
Определите, пожалуйста, свое понимание «интерфейс», а то есть ощущение, что мы о совсем разных вещах говорим.
Практическое применение, например, может быть при использовании идиомы PImpl:
И классы Attorney имеют возможность обращаться к потрохам Server::impl_.
Еще один вариант, когда публичные методы у Server невиртуальные, а приватные методы — виртуальные. И для выполнения каких-то действий снаружи нужно обращаться напрямую к приватным виртуальным методам. Как раз классы Attorney смогут это сделать.
Videoman
Послушайте, я не утверждаю, что ваш «паттерн» совсем нигде не применим. Просто хочется, чтобы задача, которую он решает, и границы его применения были бы озвучены четче, вот и все. Очевидно что он ничего не изолирует, а точнее изолирует также как и friend и private, т.е. очень слабо. В ответ вы добавляете Pimpl. Pimpl — изолирует детали реализации, безусловно, но это уже другой «паттерн». Если это просто, еще один, способ запретить вызвать из одного класса, методы другого класса, то смысла в этом маловато, по моему мнению.
eao197
Во-первых, это не мой «паттерн».
Во-вторых, хотелось бы все таки услышать ответ на прямой вопрос.
Videoman
Функционально — ничем. Только запутанней и сложнее и в случае чего сложнее для поддержки. Интерфейс дает ту же функциональность, только гибче, без потери возможностей.
eao197
Пока вы не покажете хотя бы приблизительный код — это все голословно.
Algoritmist
Если интерфейс уникальный для каждого клиента, то все равно надо будет добавляеть его в объявлении класса Server, и перекомпилировать (или я что-то упустил?).
masterspline
Общение будет более конструктивным, если ты предложишь свой вариант и объяснишь в каких случаях и почему он лучше (уверен, что не всегда).
Algoritmist
Что-то мне шаблон понравился. Через интерфейсы надо виртуальные функции делать. Следовательно, эта проверка потянет накладные расходы (это крохи, но ...), а пока все вопросы решаются на этапе компиляции.
eao197
Videoman
Ну вот не нужно только запутывать все. Естественно интерфейс имеет широкое понятие и т.д. и т.п. Под интерфейсов в «паттернах» (о чем вы пишите), всегда понимается контракт вызовов, оторванный от деталей реализации. Иначе никакого «сокрытия» не получится (или вы не раскрыли термин) или оно будет не полным.
eao197
Здесь operator+ будет частью интерфейса класса ComplexNumber.
В связи с этим уточните, когда вы говорите:
вы что именно подразумеваете?
Videoman
Отлично! Ну и напишем, что данная реализация интерфейса класса ComplexNumber защищает вас от неправильного изменения внутреннего состояния класса, от порядка вызова методов класса. От появления внешних зависимостей и от перекомпиляции кода использующего данный класс в случае его изменения, данная реализация не защищает.
eao197
Отлично что? Мой пример всего лишь показывает, что «интерфейс» в C++ — это более размытое понятие, чем интерфейс в какой-нибудь Java. Посему ваша фраза «А нужно, и всего-то, просто предоставить два интерфейса: InterfaceForClient1, и InterfaceForClient2» для C++ников не такая очевидная, как вам может показаться. Поэтому, пожалуйста, будьте добры, проиллюстрируйте свое утверждение про InterfaceForClient1 и InterfaceForClient2 каким-то примером кода.
Videoman
Ок. Тут вопрос не в иллюстрации, а в том, чего вы хотите добиться? Какую задачу вы решаете? Вам не кажется, что предоставлять публичный интерфейс и при этом его ограничивать для определенных классов это, мягко говоря, криво и большой косяк архитектуры? Если у вас возникает такая задача, то ваши классы очень сильно связаны, а это очень плохо, с точки зрения архитектуры, так как умножает количество мест, где в случае каких-либо изменений нужно будет менять код.
eao197
Пожалуйста, приведите иллюстрацию. Ибо есть подозрение, что лучше, чем вот это, у вас не получится.
Videoman
Вы хотите что бы я привел аналогичный интерфейс для абстрактного примера, ну ладно. Допустим в лоб.
eao197
Ну и:
1. Кто будет мешать дергать GetInterfaceForClientType* всем, кому не лень? Где гарантия того, что GetInterfaceForClientType1 будет вызван именно первым клиентом?
2. Как все это будет защищать от перекомпиляций при изменении потрохов Server?
3. Где и как будут создаваться экземпляры InterfaceForClient? Кто и как будет гарантировать время их жизни?
Videoman
Вы же сами просили абстрактный пример. Внешний интерфейс Server не зависит от потрохов. Для простоты, представьте что вся реализация в Pimpl.
Прелесть в том, что как угодно, вас как пользователя это не должно волновать.
Не понял? Время жизни ссылки точно такое же как время жизни самого объекта. Его гарантирует стандарт. Вы скокнули совсем в другую область. Просили абстрактный пример, я его вам привел. Допустим копирование интерфейса запрещено, и конструктор и оператор копирования = delete.
masterspline
> Тот, у кого нет описания этого интерфейса не сможет к нему обратиться.
Описания интерфейса не будет только у того, кто не включил соответствующий заголовочный файл. Так как тут, явно должен быть отдельный заголовок с ним, который будут включать тот, кто создает интерфейс и тот кто его использует. Так что защита получается забавная: включил заголовок — можешь использовать интерфейс, не включил — не сможешь.
Мне кажется, ты совсем не понял, о чем статья.
Videoman
Мне кажется это вы не понимаете в построении правильной архитектуры. Какая защита, вы о чем?! Вы часом не администратор? Кто тогда вам мешает тогда просто дописать в класс: friend class YourNewClass? В программировании основная задача, это управление растущей сложностью проекта. В правильном коде должны применяться такие конструкции, которые при развитии и сопровождении проекта потребуют как можно более локальных изменений, а не наоборот. Ваша статья решает проблему, о которой я никогда не слышал, которой просто не бывает в коде с правильно архитектурой. Friend — это «антипаттерн». Если вы создаете публичный метод, к которому принципиально имеет доступ только избранный класс, то у вас очень сильный повод задуматься над тем, что вы идете не в ту сторону и пора проводить рефакторинг кода.
eao197
Videoman
Что это, что многое и кому объясняет. Вы начинаете дискутировать с самим собой.
eao197
Вы сами признаете, что решения ничем не отличаются, но при этом ваше якобы «решение» лучше. Если бы вы удосужились сделать свой пример, в котором определение интерфейсов и их реализация, а так же объединение всего этого под одной крышей была бы доведена хотя бы до уровня примера algorithmist, то вы сами могли бы увидеть, что на счет обфускации и усложения все было бы не так однозначно.
В моей практике было случаи, когда что-то вроде Attorney приходилось использовать. Однажды это был класс, реализованный на базе PImpl, и который был связующим звеном внутри фреймворка (т.е. разные части фреймворка имели ссылку на экземпляры этого класса). И нужно было, чтобы API этого класса был поделен на «зоны» — вот эта функциональность доступна только пользователям фреймворка, вот эта — только самому фреймворку, вот эта — плагинам, которые можно дописывать для фреймворка.
Вряд ли вас такое объяснение удовлетворит, но тем не менее, на практике ситуации для Attorney и Passkey встречаются.
eao197
А теперь перечитайте свои же ответы и попробуйте объяснить, чем ваши абстрактные интерфейсы в абстрактных примерах отличаются от механизма с классом Attorney.
Videoman
Ну так и я о том же, ничем. Так зачем же заниматься усложнением и обфускацией и так используемых всеми приемов и даже не попытаться привести пример из реальной жизни и объяснить зачем нужно так все усложнять и запутывать.
Вы можете ответить на вопрос, какую реальную архитектурную проблему решает данная, так называемая, «идиома»?
ViTech
Я тоже считаю, что эти Attorney-Client и Passkey — есть костыли с ненужными сущностями при кривой архитектуре. Методы, доступные из сервера для каждого типа клиентов, должны быть сгруппированы в интерфейсы, а не из кучи кишок класса вынесены наружу. Вариантов, как это можно сделать может быть несколько. Один из них и представлен выше, может его только доработать нужно. Можно попробовать сделать как-то так:
qw1
Это решение совсем другой задачи.
Автор статьи пытается сделать проверку в compile-time и с нулевыми накладными расходами.
Здесь же проверка делается в runtime (canGrantAccessTo), причём возможна эксплуатация в стиле
Если делать в таком стиле, то уж с настоящей авторизацией:
ViTech
А если убрать canGrantAccessTo, то получится «проверка в compile-time и с нулевыми накладными расходами»? Упор в примере совсем на другое-то сделан. canGrantAccessTo — это дополнительная функциональность, при появлении хотелки разрешать доступ не только по типу класса, но и по существующему объекту. И под авторизацией не обязательно понимается проверка логина и пароля.
Эксплуатаций можно много придумать, вплоть до внедрения Паблика Морозова ).
masterspline
> Я тоже считаю, что эти Attorney-Client и Passkey — есть костыли с ненужными сущностями при кривой архитектуре.
Вообще, любая нетривиальная идиома появляется, потому что тривиальное решение «не очень» и его хочется улучшить.
> Методы, доступные из сервера для каждого типа клиентов, должны быть сгруппированы в интерфейсы, а не из кучи кишок класса вынесены наружу. Вариантов, как это можно сделать может быть несколько. Один из них и представлен выше, может его только доработать нужно. Можно попробовать сделать как-то так:
Если из сколь угодно сложного решения выкинуть детали реализации, то в общих чертах все будет очень просто. Ты в своем решении не привел реализации интрефейсов, в результате оно выглядит простым.
клиент доберется до сервера.
вообще не нужен.
Идея использовать что-то похожее на Visitor и вообще не пытаться контролировать кому даешь доступ, а предоставлять его конкретному экземпляру, использую перегрузку, интересный подход, но только если нужен контроль в runtime.
ViTech
Это похоже на интригу ). А что клиенту от сервера нужно? Весь необходимый набор методов в этом интерфейсе и передаётся.
Проверку canGrantAccessTo никто и не навязывает.
Лучше тем, что нет friend, и левых сущностей, не относящихся к предметной области.
masterspline
И еще один вопрос про
Кто и когда будет уничтожать экземпляр интерфейса? Он будет создаваться для каждого экземпляра клиента и уничтожаться при в его деструкторе или это будет разделяемый между клиентами экземпляр? Но это уже слегка offtop про memory leak и exception safety, который показывает степень «проработанности» идеи.
ViTech
Это уже совсем другая задача. В примере я так написал для краткости. Если сервер напрямую реализует интерфейс InterfaceForClient1 (наследуется от него), то можно передать просто this. Это если с raw-указтелями работать, чего делать совсем не стоит. И нужно мутить как минимум с умными указателями.
Это вопрос из той же серии, почему в вашем примере сервер передаётся по ссылке, и есть ли уверенность, что этот сервер будет жить дольше всех его клиентов.
eao197
Простите, а вы не пытаетесь в C++ привычки из Java перенести? Уверены, что new InterfaceForClient1() для предоставления доступа к некой части функциональности Server-а — это разумная цена? А то ведь подход на базе Attorney вообще накладных расходов не имеет.
Сами придумали или подсмотрели где-то?Ну а это просто прекрасно:
ViTech
Я хочу перенести привычки из хорошей архитектуры. Это лишь один из вариантов, причём набросок, а не конечное решение. Без friend и непонятных сущностей. Вдруг кто из него что-то полезное почерпнёт.
Я с интрудерами только так и поступаю. А Вы?
eao197
И откуда такая ненависть к friend? Вот таком контексте вы так же против friend-а?
А мне как-то не доводилось встречать случаев, когда бы разработчик классов, подобных Server, знал бы про существование Intruder-ов и подобных ему классов.
ViTech
Я рад, что вам понятно ). Отделять интерфейсы от реализации это я только что придумал, и перспективы этого весьма туманны.
friend давно разжевали, нет необходимости ещё раз это делать. Вы лучше расскажите, как будет приятно везде этот Passkey таскать, и других убедить это делать.
eao197
Ирония вместо технических аргументов и примеров кода — это отличный способ завершить спор. Тем более, что вот эти слова:
явно демонстрируют уровень понимания сути проблемы.qw1
ViTech
Полнее код пишите, пожалуйста. В каких классах какие методы будут с таким параметром по умолчанию?
qw1
Ниже полностью работающий пример, на основе авторского
masterspline
Так делать нельзя, я об этом написал в статье (сам думал, что можно, но в таком случае этот метод сможет вызвать кто угодно).
qw1
да, действительно
qw1
Напомнило
Причём в Release-сборке delete может упасть, а может по-тихому испортить память. Но с коллегами по проекту, не умеющими использовать ваш класс, только так и надо.
ViTech
Об этом и речь: что и от кого защищаем?
Достаточно злодейское действие?
qw1
Я думаю, тут такой сценарий. Архитектор или Senior решили, что с Server взаимодействуют только определённые классы, и это ограничение явно прописали в код Server.
Какой-нибудь Junior может, забыв о договорённостях, напрямую вызывать Server из своего класса (условно, Intruder). Причём изменения в Server тщательно ревьюятся, чтобы не допустить туда добавления новых friends, а изменения всех Intruders (читай — говноклассов бизнес-логики) проверять лень.
Нужно, чтобы пролезть в сервер было максимально сложно, не меняя его исходников.
Algoritmist
Через интерфейсы будет удобнее.
Вообще, считаю появление friend — поводом задуматься об архитектуре.
Если вернуться к статье, то последний вариант (Passkey)
Algoritmist
Согласен, мой пример хуже Passkey.
masterspline
> Через интерфейсы будет удобнее.
Приведи пример кода, пожалуйста. Чтобы сравнить с описанными вариантами. По моим понятиям, в первом варианте class Attorney — вполне себе интерфейс. Но, думаю, ты под интерфейсом понимаешь что-то другое.
> Вообще, считаю появление friend — поводом задуматься об архитектуре.
Одна из мыслей, которые меня преследуют с появления идеи написания статьи, что архитектуру моего приложения можно сделать лучше. И одна из целей статьи — улучшить знания и навыки проектирования приложений (это ж не корпоративный блог, а написать хорошую статью не так просто, кто пробовал — знает).
Algoritmist
Да, у меня без friend не выходит (и для этой задачи, похоже не выйдет).
Под вариантом с «интерфейсом» я понимал
Door
По поводу интерфейсов — тут уже говорили. И мне тоже кажется, что они здесь более уместны.
С интерфейсами — будет проще и понятней разобраться новичку, они более четко документируют код. И, что тоже важно, уменьшают количество "дополнительных абстракций", в которых тоже приходится разбираться, читая код.
Что я имею в виду, так это такой код:
Фишка в том, что код до безобразия простой и понятный, а для тех, кто волнуется по поводу рантайма — есть несколько аргументов.
Server
какfinal
) и он может убрать виртуальный вызов.clang -O3:
gcc -O3:
и… Visual C++ не справился
Как видите — кое-кто смог убрать виртуальные колы (и это не потому, что ф-и очень простые — если они не заинлайнятся — так всё равно будет прямой вызов к экземпляру
Server
… я надеюсь)masterspline
Подобный пример мне уже приводили еще вчера вечером. Я уже тогда был уверен, что виртуальные вызовы в Server будут оптимизированы, если класс объявить final. В этом решении суть в том, что оно не решает поставленную задачу. Intruder вполне может сделать привидение (cast) к IClientAccess и получить доступ клиента.
Думаю причина большого числа возражений в том, что:
Могу предложить такую задачу: есть класс Server (или с другим именем), который как-то обрабатывает данные. Данные на обработку можно отправлять через открытый интерфейс. У него внутри есть стратегия обработки данных, которую можно менять, но только если вызов на замену приходит от определенного класса (Manager). Таким образом, я не хочу, чтобы любой клиент сервера мог сменить стратегию расчета. Или, например, у меня есть вызов, который ставит данные на обработку с более высоким приоритетом, но он должен быть доступен только избранным.
Пример:
Такой вот класс, рефакторить тут особо нечего. Нужно ограничить доступ к div() только избранным классам, а в детали реализации вообще никто лезть не должен. По сути div() часть интерфейса класса, но к ней нужно ограничить доступ.
ViTech
Поэтому хотелось бы увидеть пример, аналогичный реально используемому. Сколько у вас, в среднем, у классов типа Server таких вот friend'ов? Клиенты могут хранить связь с сервером (ссылка, указатель разной степени умности)? Как эта идиома чувствует себя в многопоточной среде?
Проблема с разграничением доступа к объекту появилась совсем недавно, в начале 2017 года? Раньше таких задач не возникало? Лично я как-то не очень наблюдаю повсеместного использования Passkee. А вы?
eao197
Его повсеместное применение и не нужно. Это частное решение для очень узкого круга задач, когда другие, более очевидные подходы (например использование интерфейсов в стиле языка Java) не работают или есть какие-то противопоказания к их применению.
ViTech
Тут начали такты считать на вызов виртуальных методов, вот мне и стало интересно, насколько хорошо решены накладные расходы на многопоточность.
Поэтому и хотелось бы увидеть в статье более подробное описание области, в которой предложенная идиома может хорошо себя показать (кроме того, что она такая, красивая, в принципе есть). Лучше расписать её достоинства, недостатки и ограничения. В идеале ещё привести сравнение с альтернативными решениями.
eao197
Думаю, у автора была задача просто рассказать о наличии таких идиом. Ибо если идиому Attorney многие переизобретают даже не подозревая об этом, то вот Passkey более хитрая штука и не зная про ее изобрести ее «с нуля» смогут далеко не все. А анализ и сравнение — это уже тема для отдельной статьи.
Door
ну вы как-то слишком с "любителями красивой архитектуры". С Passkey и Attorney публичный интерфейс Server-а обрастает ненужными знаниями: почему я должен при добавлении нового клиента идти в Server.h и добавлять forward declaration для нового клиента? Как-то нелогично, что Server знает почти всех своих пользователей. В конце-концов, если утрировать, вы же не идёте в класс
std::basic_string<>
и не добавляете обьявление своего класса/не важно чего, который хочет использовать строки?Вот, комментарий ниже — там уже идёт попытка избавиться от дублирования и прочее при появлении нового клиента.
Получается так, что всё хорошо, если мы будем придерживаться очень специфичных входных условий "количество клиентов — 1 или 2, есть 2 ф-и, нужно распределить доступ к ним между этими клиентами"
(на заметку — мне лично решение симпатизирует. Просто идёт рассуждение про применимость этой идиомы для других случаев с кучей вопросов "а что если")
eao197
У меня сложилось ощущение, что у многих комментаторов здесь нет понимания, что Passkey и Attorney — это эдакий «лом», который стоит применять там, где остальные, более удобные, простые и безопасные идиомы не подошли по тем или иным причинам. Но уж в этих случаях по эффективности и другим параметрам альтернативу Passkey и Attorney подобрать не так просто.
Door
угу. К примеру, у Eiffel есть возможность указывать список классов, которым доступна ф-я:
В этом случае, Passkey и Attorney — просто попытка добиться того же. Как и почти всегда — в случае с C++ — круто то, что язык позволяет сделать тебе такой воркэраунд — как следствие — воркэраунд он на то и воркэраунд — вызывает много вопросов.
Лично я не фанат возможности указывать "именно список классов, которым разрешается доступ к фиче". Возможно поэтому и спорю :)
ViTech
Как вы думаете, в чём причина того, что комментаторы это не понимают? В том что они не очень умные, или в том, что в статье об этом мало написано? Для меня классы из статьи выглядят публичными классами какой-то библиотеки, а не деталями её реализации. По-хорошему, детали реализации не должны быть доступны пользователям публичных классов и пишутся обычно небольшой группой разработчиков. Они не доверяют друг другу?
eao197
Это ваше личное мнение, сложно судить, почему оно у вас именно такое.
Я бы смотрел на это таким образом:
* может быть ситуация, когда разрабатывается большой фреймворк, состоящий из нескольких модулей;
* в одном из основных модулей определяется некий класс A, который должен быть доступен всем (как остальным модулям фреймворка, так и пользователям фреймворка);
* части модулей фреймворка от класса A может потребоваться какая-то часть функциональности, объявленная в private-секции класса. Поскольку мы находимся внутри фреймворка и, в принципе, понимаем что делаем, то такой доступ можно дать. Но только части модулей самого фреймворка. Пользователи фреймворка доступа к этой функциональности иметь не должны.
Идиомы Attorney и Passkey вполне уместны здесь. А в реализации могут быть более удобными и дешевыми, чем какие-то другие схемы. При этом, в принципе, разные модули фреймворка могут разрабатываться разными людьми, для разных задач с разными требованиями. Поэтому может быть разный набор Attorney/Passkey.
ViTech
То же самое можно сказать и про ваше личное мнение, что в статье автор ведёт речь про детали реализации, а не про публичные классы. На чём основана такая уверенность? В статье есть какие-нибудь предпосылки к такому мнению?
Читатель с «недостатком опыта работы именно с C++» увидит эту статью, ему понравятся эти решения и он решит применить их в своих проектах. Вы уверены, что он со своим уровнем опыта применит их правильно, в деталях реализации, а не потащит в публичную часть? За опытных разработчиков я не переживаю ).
Это хорошо, что вы объясняете. Странно, что не автор это делает. Читателям статьи, чтобы уточнить существенные детали по применимости этой идиомы, теперь придётся извлекать их из груды комментариев.
eao197
После прочтения статьи у вас сложилось ваше впечатление. Я не знаю, почему оно у вас сложилось именно таким. Может на основании вашего опыта. Может вы не выспались. Может перед этой статьей читали какую-то другую статью и одно на другое наложилось. Я не могу сказать, почему вы восприняли статьи именно так.
Исходя из своего опыта я просто более-менее представляю, где оба описанных подхода могут использоваться. И для меня эта статья полезна именно тем, что в ней коротко описываются детали каждой из идиом без лишних объяснений зачем и для чего нужны сами идиомы. Такой нормальный справочный материал.
Может у человека просто времени нет на то, чтобы написать хороший обзор нескольких подходов и сравнительный анализ их применимости, выгод и недостатков.
ViTech
В том:
Как я уже сказал, за опытных разработчиков я не переживаю. Я не уверен, что остальные читатели столь же опытны, как вы, и не хотел бы чтобы они применяли предлагаемые решения неправильно. Пусть они об этом хотя бы из комментариев узнают.
eao197
Но ведь это ваша фраза. Я такого не говорил, соответственно, мне не понять, откуда у меня может быть уверенность в том, о чем я не говорил.
ViTech
Хорошо. Тогда расскажите, откуда у вас появилось
и что автор именно «эдакий лом» в статье и подразумевал. И только неопытные комментаторы об этом не догадались и не поняли.
eao197
Так здесь же много правильных слов сказали о том, что friend — это нарушение инкапсуляции. Где-то это нарушение оправдано (например, когда определяется friend void swap), но в большинстве случаев friend — это маркер, который означает, что другого, более безопасного решения не нашлось. Тому может быть несколько причин (навскидку):
* у человека просто не было опыта и он влепил friend не понимая, чем это может горозить в исторической перспективе;
* решение с friend является quick-and-dirty временным решением, которое сделали ибо не было возможности выполнить нормальный рефакторинг;
* решение с friend объективно лучше по совокупности каких-то параметров и оно оправдано в данном контексте.
Поскольку описание того же Passkey мне доводилось видеть и раньше, а так же доводилось применять Attorney несколько раз, то у меня уже было представление о ситуациях, в которых такие идиомы оправданы. И это именно ситуации, когда нужно что-то из категории «против лома есть приема».
В статье об этом не говориться. Но как я уже сказал, для меня статья ценна тем, что она больше похоже на справочный материал, а не на учебник для новичков.
Videoman
А у меня складывается ощущение, что у вас мания величия и кроме вас С++ больше никто не знает (хотя от части так и есть).
Основные претензии к методам показанным в статье:
1. То что это называется «идиома» — т.е. новички в С++ могут подумать что это устоявшиеся полезные практики и начать это использовать повсеместно. Хотя, запихивание разных не связанных интерфейсов в один объект — это «антипаттерн»
2. То, что данные подходы, якобы, не предоставляют доступа к деталям реализации — сколько раз уже повторялось, что private секция является очень слабым способом изоляции, почти нулевым. Все что там находится, все участвует в разрешении имен, перегрузках, тащит за собой инклюды и засоряет пространство имен кода использующих класс.
Вы так и не смогли объяснить, зачем так писать код, чтобы нужно было потом использовать такие финты. За 17 лет коммерческой разработки на С++ у меня ни разу не было ситуации, когда «God Object» нельзя было бы разделить на меньшие, каждый со своим интерфейсом.
«RAII» — идиома
«Copy and Swap» — идиома
«Non-virtual interface» — идиома
По каждой я могу объяснить как она работает и какие проблемы решает, а то что приведено в статье — это вредный финт ушами, так как к сторонней библиотеке это не применишь, а свой код так не напишешь.
eao197
По вашим словам, «Non-virtual interface» — это тоже идиома. И что, новички должны брать и использовать ее повсеместно?
А что, обещал? Мне приходилось использовать что-то вроде Attorney. Примеры вам приводились. То, что вас они не устраивают, ну это же не мои проблемы.
Т.е. эти 17 лет являются мерилом, которое позволяет вам глаголить абсолютную истину? Прекрасно. Не понятно, правда, почему вы тогда на простые вопросы внятно и с примерами кода ответить не можете.
Ну и да, 17-тью годами вы не того человека напугать решили, спрячьте линейку, такой длиной хвататься неприлично.
Videoman
Очень быстро отвечаете, явно не успеваете подумать, а зря, хорошему программисту всегда лучше сначала подумать, а потом уже писать.
По-моему, здесь ресурс для обмена мнениями, разве нет.. могут, нет проблем. «Зачем так писать ?» -здесь вопрос риторический. Не нужно распространять плохие практики. Видно, что вам никогда не приходилось пытаться поддерживать такой ужас на friend-ах — вроде классы есть, а на самом деле один большой класс, который не расширить, не изолировать.Дальше пошло уже хамство. Я так, понимаю что вы человек, которые не терпит точку зрения отличную от вашей, так что желаю вам успехов. Может лет через 17 вы поймете что я хотел до вас донести.
eao197
Только для этого нужно взять и показать хотя бы одно нормальное альтернативное решение. После чего провести сравнение по каким-то параметрам. Это труд, который вы на себя не берете.
Вот тут показали возможное решение. Как-то по объему и сложности оно не шибко выигрывает. Об этом вам здесь и говорят столько времени. Но вместо того, чтобы прислушаться, вы пытаетесь аппелировать к своему опыту (и явно нарываетесь не на тех), а потом обвиняете собеседника в хамстве. Ведите разговор конструктивно, отношение к вашим словам будет соответствующим.
eao197
Собственно, поэтому я и хотел получить от вас вменяемые ответы на которых можно было бы уже рассуждать, что выгодно, что нет, когда и где. Вместо этого вы пытаетесь аппелируя к своему 17-тилетнему опыту заявлять, что friend плохо и private ни от чего не защищает и т.д., и т.п. Что к делу имеет косвенное отношение.
qw1
А в выводе компилятора указана нераспознанная опция:
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
Door
Да, конечно. Это я игрался с опциями и компиляторами. Перепробовал для cl разные варианты (
/Ox
,/O2
и т.д.), чтобы избавиться от виртуал кола. В итоге, ничего не получилось, я переключился на другие компиляторы, а ссылку для cl — уже в конце вставил, чтобы было справедливо, что не все смогли.qw1
Поигрался сам с компилятором. И в x86, и в x64 получается интересная штука:
первый вызов не виртуальный (по адресу функции int client_access()), второй вызов виртуальный, затем сложение.
Получается, MSVC при использовании множественного наследования оптимизирует виртуальные вызовы только из первого предка.
Door
Интересно. Спасибо за заметку
Videoman
Согласен насчет friend-a. Вообще friend обеспечивает самую слабую степень инкапсуляции и является костылем для случаев, когда у вас нет другого выхода. Например, в случае каких-то сторонних библиотек или legacy.
Algoritmist А вам не кажется что у вас просто Visitor получился?
Algoritmist
Да, что-то погорячился…
ViTech
Что насчёт наследования от Client? Могут ли наследники Client обращаться к серверу на правах своего предка? В постановке задачи что об этом говорится?
qw1
Не могут
friend
не наследуется.ViTech
Это в текущей реализации так получается. А я про постановку задачи спрашивал.
qw1
Если разрешить доступ всем потомкам, получается слишком очевидная дыра: наследуешься от Client и дописываешь метод (можно static, чтобы экземпляр не создавать), в котором делаешь, что хочешь.
ViTech
А если не разрешать доступ потомкам, то интерфейсы типа AbstractClient в виде абстрактных классов (если такие вдруг понадобятся) оказываются в пролёте, я правильно понимаю? Поэтому и хотелось бы уточнить область применения этой идиомы. Если это не светится в публичной части, а используется где-то глубоко в дебрях реализации, стоит ли такой огород там городить?
qw1
Неправильно понимаете. Пользователь интерфейса AbstractClient вообще не знает, какие отношения с конкретным Server у данного ему AbstractClient, это не его поле интересов.
Писал выше — если header-файл сервера доступен (а он может быть нужен, т.к. от него зависит Client), то любой неосторожный разработчик напрямую может залезть в сервер, наплевав на рекомендации по архитектуре, что нежелательно.
В принципе, pImpl решает этот вопрос, но без zero-cost, и с другими проблемами.
nikabc555
Очень интересный метод с использованием Passkey
Но в вашей реализации мне не нравится, что proxy_some_method копипастится для каждого из клиентов. Так можно и ошибок наплодить, и при смене сигнатуры придется править каждую копию proxy_some_method
По-моему, лучше предоставить компилятору клонирование proxy с помощью шаблонов. А выбор классов для доступа организовать с помощью type_traits и static_assert
В итоге получится подобное:
masterspline
Я пытался сделать, чтобы в Passkey<> можно было передавать несколько параметров
Но не осилил, чтобы из variadic template можно было сгенерировать список friend
Чтобы соответствующий Passkey<> могли создать любой из перечисленных классов. Тогда можно было бы создавать один
и вызывать его
Хотя вполне можно создать passkey.hpp с 1024 вариантами шаблона Passkey<> для 1...1024 шаблонных параметров.
iCpu
Я правильно понимаю, что это просто извращённый вид старой доброй условной компиляции? Если да, то чем ваше решение лучше небольшого шаблона?
https://ideone.com/9pQayU
https://godbolt.org/g/AED1wJ
ViTech
У меня ещё такие вопросы возникают: