Данная статья является авторским переводом с английского собственной статьи под названием God Adapter. Вы также можете посмотреть видео выступления с конференции C++ Russia. В статье представлен специальный адаптер, который позволяет оборачивать любой объект в другой с дополнением необходимой функциональности. Адаптированные объекты имеют один и тот же интерфейс, поэтому они полностью прозрачны с точки зрения использования. Будет последовательно введена общая концепция, использующая простые, но мощные и интересные примеры. ПРЕДУПРЕЖДЕНИЕ. Почти все методы, указанные в статье, содержат грязные хаки и ненормальное использование языка C++. Так что, если вы не толерантны к таким извращениям, пожалуйста, не читайте эту статью. Термин универсальный адаптер происходит от возможности универсальным образом добавить необходимое поведение для любого объекта. Давным давно я представил концепцию умного мьютекса для упрощения доступа к общим данным. Идея была простой: связать мьютекс с данными и автоматически вызывать lock и unlock при каждом доступе к данным. Код выглядит следующим образом: Но в этом подходе есть несколько проблем. Блокировка держится в течении всего времени выполнения текущего выражения. Рассмотрим следующую строку: Разблокировка вызывается после завершения выполнения всего выражения, включая вывод в std::cout. Это ненужная трата времени, что значительно увеличивает время ожидания при взятии блокировки. Как следствие первой проблемы, существует возможность взаимной блокировки из-за неявного механизма блокировки и длительного времени блокировки при выполнении текущего выражения. Рассмотрим следующий фрагмент кода: Совершенно неочевидно, что функция потенциально содержит взаимную блокировку. Это происходит из-за того, что метод ->get() можно вызывать в любом порядке для разных пар экземпляров x и y. Таким образом, было бы лучше избегать увеличения времени взятия блокировки и не допускать упомянутые выше взаимные блокировки. Идея довольно проста: нам нужно внедрить функциональность прокси-объекта внутрь самого вызова. А чтобы упростить взаимодействие с нашим объектом, заменим -> на .. Проще говоря, нам нужно преобразовать объект Data в другой объект: В этом случае мы контролируем операции получения и освобождения мьютекса внутри самих методов. Это предотвращает проблемы, упомянутые ранее. Но такая запись неудобна для реализации, потому что базовая идея умного мьютекса заключается в том, чтобы избежать дополнительного кода. Предпочтительный способ — это использовать преимущества обоих подходов: меньше кода и меньше проблем одновременно. Таким образом, необходимо обобщить это решение и распространить его для более широких сценариев использования. Нам нужно как-то адаптировать нашу старую реализацию Data без mutex для реализации, содержащей mutex, которая должна выглядеть аналогично классу DataLocked. Для этого обернем вызов метода для дальнейшей трансформации поведения: Здесь мы откладываем вызов data.set(v) и передаем его в T_base::call(lambda). Возможная реализация T_base может быть такой: Как вы можете видеть, мы разделили монолитную реализацию класса DataLocked на два класса: DataAdapter<T_base> и MutexBase как один из возможных базовых классов для созданного адаптера. Но фактическая реализация очень близка: мы удерживаем мьютекс во время вызова Data::set(v). Давайте еще обобщим нашу реализацию. У нас MutexBase реализация работает только для Data. Улучшим это: Здесь использовано несколько обобщений: Таким образом, мы можем определить: При использовании обобщений невозможно остановиться. Иногда я хотел бы преобразовать входные параметры. Для этого я изменю адаптер: И реализация BaseLocker преобразуется в: Наконец, давайте уменьшим размер шаблонного кода, связанный с адаптером. Шаблоны заканчиваются и в ход вступают продвинутые макросы с итераторами: DECL_FN_ADAPTER позволяет обернуть любой метод с именем D_name. Теперь осталось лишь перебрать все методы объекта и обернуть их: Теперь мы можем адаптировать наш Data, используя лишь одну строку: И все! Мы рассмотрели адаптер на основе мьютекса. Рассмотрим другие интересные адаптеры. Иногда нам зачем-то нужно использовать shared_ptr для наших объектов. И было бы лучше скрыть это поведение от пользователя: вместо использования operator-> хотелось бы просто использовать operator.. Ну или хотя бы просто .. Реализация очень проста: Применение: Иногда возникает отличная идея пошарить данные между потоками. Общая схема состоит в объединении shared_ptr с mutex. shared_ptr решает проблемы с временем жизни объекта, а mutex используется для предотвращения состояния гонки. Поскольку каждый адаптированный объект имеет тот же интерфейс, что и оригинальный, мы можем просто объединить несколько адаптеров: С таким использованием: Шагнем в будущее. Например, у нас есть следующий интерфейс: Но мы хотели бы использовать асинхронный интерфейс будущего: Где Future имеет следующий интерфейс: Соответствующий адаптер: Применение: Т.к. это направляет нас в прошлое, то пусть это будет домашней задачей. Разработчики ленивы. Давайте адаптируем любой объект для совместимости с разработчиками. В этом контексте ленивость означает создание объекта по требованию. Рассмотрим следующий пример: Т.е. идея состоит в том, чтобы оттягивать создание объекта до последнего. Если пользователь решил использовать объект, мы должны его создать и вызвать соответствующий метод. Реализация базового класса может быть такой: И теперь мы можем создать тяжелый ленивый объект и инициализировать его только в случае необходимости. При этом он полностью прозрачен для пользователя. Давайте рассмотрим производительность адаптера. Дело в том, что мы используем лямбды и переносим их в другие объекты. Таким образом, было бы крайне интересно узнать накладные расходы таких адаптеров. Для этого рассмотрим простой пример: обернем вызов объекта, используя сам объект, т.е. создадим тождественный адаптер и попытаемся измерить накладные расходы для такого случая. Вместо того, чтобы делать прямые измерения производительности, давайте просто посмотрим на сгенерированный код ассемблера для разных компиляторов. Во-первых, давайте создадим простую версию нашего адаптера для работы только с методами on: BaseValue — это наш тождественный базовый класс для вызова методов непосредственно из того же типа T: И вот наш тестовый класс: Ниже вы можете найти результаты, полученные в онлайн-компиляторе: GCC 4.9.2 Clang 3.5.1 Как можно видеть, здесь нет никакой разницы между f1 и f2, что означает, что компиляторы могут оптимизировать и полностью устранять накладные расходы, связанные с созданием и передачей лямбда-объекта. В статье представлен адаптер, который позволяет преобразовать объект в другой объект с дополнительной функциональностью, который оставляет неизменным интерфейс без накладных расходов на преобразование и вызов. Классы базового адаптера — универсальные трансформеры, которые могут быть применены к любому объекту. Они используются для улучшения и дальнейшего расширения функциональности адаптера. Различные комбинации базовых классов позволяют легко создавать очень сложные объекты без дополнительных усилий. Эта мощная и занимательная техника будет использована и расширена в последующих статьях. [1] github.com/gridem/GodAdapter
[2] bitbucket.org/gridem/godadapter
[3] Blog: God Adapter
[4] Доклад C++ Russia: Универсальный адаптер
[5] Видео C++ Russia: Универсальный адаптер
[6] Хабрахабр: Полезные идиомы многопоточности С++
[7] Онлайн компилятор godbolt
 Software Engineer