Что такое sfinae и pimpl
Перевод статьи «Pimp My Pimpl», часть 1
Ссылки на оригинал
Это перевод первой части статьи с сайта Heise Developer. Оригиналы обеих частей находятся здесь: часть 1, часть 2
Перевод был сделан с английского перевода отсюда.
Аннотация
Многое было написано об идиоме со смешным названием Pimpl (прим. пер.: созвучно с англ. pimple — прыщ), также известной как d-указатель, фаерволл компилятора и Чеширский кот. Heise Developer освещает некоторые стороны этой практической конструкции, которая выходит за рамки классической техники.
Классическая идиома
Каждый программист на C++ наверное сталкивался с описанием класса наподобие этого:
В языке C такой подход хорошо работал: детали реализации функций полностью инкапсулировались с помощью разделения объявления и описания; в нем можно либо сделать только предварительное объявление структур (в этом случае они будут приватными), либо описать их прямо в заголовочном файле (тогда они будут публичными). В «объектно-ориентированном C» вышеприведенный класс Class может выглядеть следующим образом:
К сожалению, это не работает в C++. Методы должны быть объявлены внутри класса. Классы без методов были бы бесполезны, поэтому в заголовочных файлах C++ обычно присутствуют описания классов. Так как тело класса, в отличие от пространства имен, не может быть повторно открыто, заголовочный файл должен содержать все объявления (полей данных и методов):
Проблема очевидна: интерфейс модуля (заголовочный файл) обязательно содержит детали реализации — плохой подход. Поэтому используется довольно грубый трюк, когда все детали реализации (поля данных и приватные методы) выносятся в отдельный класс:
Таким образом, мы получаем удобства системы полностью инкапсулируемых модулей в C++. Из-за применения промежуточной переменной, за полученные преимущества приходится платить накладным выделением памяти ( new Class::Private ), косвенными обращениями к полям данных, а также полным отказом (по крайней мере в секции public) от inline методов. Как будет показано во второй части статьи, семантика константных методов также изменяется.
Перед второй частью данной статьи, посвященной исправлению или, по крайней мере, смягчению вышеуказанных недостатков, попробуем описать выгоды, которые дает применение рассматриваемой идиомы.
Преимущества идиомы Pimpl
Преимущества использования Pimpl существенны. Инкапсулируя все детали реализации, получаем тонкий и долгосрочный стабильный интерфейс (заголовочный файл). Под первым подразумевается легко читаемое описание класса; под вторым — поддержка бинарной совместимости даже после значительных изменений в реализации.
Например, отдел Nokia «Qt Development Frameworks» (ранее Trolltech) по крайней мере дважды за время разработки библиотеки классов «Qt 4» вносил глубокие изменения в рендеринг виджетов без необходимости перекомпоновки (relink) приложений, использующих Qt 4.
Не стоит недооценивать значительное ускорение сборки при использовании идиомы Pimpl, особенно в больших проектах. Ускорение сборки происходит из-за уменьшения количества директив #include в заголовочных файлах и по причине значительного уменьшения частоты внесения изменений в заголовочные файлы классов Pimpl. В книге «Решение сложных задач на C++» («Exceptional C++») Герб Саттер отмечает неизменное удвоение скорости компиляции, а Джон Лакос даже утверждает, что сборка ускоряется на два порядка.
Еще одно достоинство применения Pimpl: классы с d-указателями хорошо подходят для транзакционно-ориентированного и безопасного относительно исключений кода. Например, разработчик может использовать идиому Copy-Swap (Саттер, Александреску «Стандарты программирования на C++», пункт 56), чтобы создать транзакционный (всё-или-ничего) копирующий оператор присвоения:
Реализация операций перемещения в C++0x тривиальна (и, в частности, одинакова для всех классов Pimpl):
Расширенные способы композиции
Последнее преимущество Pimpl, которое стоит отметить, это возможность сократить дополнительные динамические выделения памяти, используя прямую агрегацию полей данных. Без использования Pimpl агрегацию принято делать с помощью указателей, чтобы отделить классы друг от друга (применение Pimpl для полей данных). Применяя Pimpl целиком для всего класса, можно избавиться от необходимости хранить приватные данные сложных типов только по указателям.
Например, идиоматический класс диалога Qt
Знатоки Qt могут заметить, что деструктор QDialog уже и так разрушает виджеты потомков, следовательно, прямая агрегация приведет к двойному вызову их разрушения. Действительно, использование этой техники создает угрозу появления ошибок последовательности распределения памяти (двойное удаление, использование после освобождения и т.д.), особенно если поля данных также принадлежат классу и наоборот. Тем не менее, показанное преобразование в данном случае безопасно, т.к. Qt всегда позволяет удалять потомков перед их родителями.
Кроме того, при таком подходе компилятор имеет гораздо больше шансов на «девиртуализацию» вызовов виртуальных функций, т.е. он удалит двойные косвенные вызовы, к которым приводит виртуальность вызываемых функций. При использовании агрегации по указателю это требует межпроцедурной оптимизации. В любом случае, это даст выигрыш производительности во времени выполнения на фоне дополнительных косвенных вызовов; тем не менее d-указатель должен быть проверен по мере необходимости с помощью профилирования конкретных классов.
Промежуточные выводы
Pimpl — хорошо известная идиома C++, которая позволяет программисту отделить интерфейс класса от его реализации в такой степени, как того не позволяет сделать C++ напрямую. Положительными побочными эффектами использования d-указателя являются ускорение компиляции, упрощение реализации семантики транзакций и возможность сделать реализацию, потенциально более эффективную во времени выполнения, используя расширенные способы композиции.
Во второй части статьи автор покажет решение для некоторых из перечисленных проблем.
Сложность увеличится еще сильней, так что в каждом случае нужно проверять, перевешивают ли преимущества применения идиомы её недостатки. В случае сомнений, такую проверку необходимо делать для каждого сомнительного класса. Как всегда, здесь нет общего решения.
Что дальше?
Вторая (и последняя) часть этой статьи познакомит нас с внутренним устройством Pimpl, раскроет проблемные места и дополнит идиому с помощью целого ряда улучшений.
Иммутабельные данные в C++
Привет, Хабр! Об иммутабельных данных немало говориться, но о реализации на С++ найти что-то сложно. И, потому, решил данный восполнить пробел в дебютной статье. Тем более, что в языке D есть, а в С++ – нет. Будет много кода и много букв.
О стиле – служебные классы и метафункции используют имена в стиле STL и boost, пользовательские классы в стиле Qt, с которой я в основном и работаю.
Введение
Что из себя представляют иммутабельные данные? Иммутабельные данные – это наш старый знакомый const, только более строгий. В идеале иммутабельность означает контекстно-независиую неизменяемость ни при каких условиях.
По сути иммутабельные данные должны:
Иммутабельные данные пришли из функционального программирования и нашли место в параллельном програмировании, т. к. гарантируют отсутсвие побочных эффектов.
Как можно реализовать иммутабельные данные в С++?
В С++ у нас есть (сильно упрощенно):
Функции и void не имеет смысл делать иммутабельными. Ссылки тоже не будем делать иммутабельными, для этого есть const reference_wrapper.
Что касается остальных вышеперечисленных типов, то для них можно сделать обертки (а точнее нестандартный защитный заместитель). Что будет в итоге? Цель сделать как-бы модификатор типа, сохранив естественную семантику для работы с объектами данного типа.
Интерфейс
Общий интерфейс прост – всю работу выполняет базовый класс, который выводится из характеристик (traits):
Запрещая оператор присваивания, мы запрещаем перемещающий оператор присваивания, но не запрещаем перемещающий конструктор.
immutable_impl что-то вроде switch, но по типам (не стал делать такой – слишком усложняет код, да и в простом случае он не особо нужен – ИМХО).
В качестве ограничений явно запретив все операции присваивания (макросы помогают):
А теперь давайте рассотрим как реализованы отдельные компоненты.
Иммутабельные значения
Под значениями (далее value) понимаются объекты фундаментальных типов, экземпляры классов (структур, объединений), перечислений. Для value у на есть класс, который определяет является ли тип классом, структурой или объединением:
Если да, то для реализации используется используется CRTP:
Но решение по этому вопросу ожидается в марте, поэтому для этих целей пока используем оператор ():
Обратите внимание, на конструктор:
там инициализируется как immutable_value, так и базовый класс. Это позволяет осмысленно манипулировать с immutable_value через operator (). Например:
Если же тип является встроенным, то реализация будет один-в-один, за исключением базового класса (можно было бы из ъ вернуться, чтобы соответствовать DRY, но как-то не хотелось усложнять, тем более, что immutable_value делался после остальных. ):
Иммутабельные массивы
Пока вроде бы просто и неинтересно, но теперь примемся за массивы. Надо сделать что-то вроде std::array сохранив естественную семантику работы с массивом, в том числе для работы с STL (что может ослабить иммутабельность).
Особенность релизации заключается в том, что при обращении по индексу к многомерному возвращается массив меньшей размерности, тоже иммутабельный. Тип массива рекурсивно инстанцируется: см. operator[], а конкретные типы для итераторов и т.д выводятся с помощью array_traits.
Для определения типа меньшей размерности используется класс характеристик:
который для многомерных массивов для при индексировании возвращает иммутабельный массив меньшей размерности.
Операторы сравнения очень просты:
Иммутабельный итератор
Для работы с иммутабельным массивом используется иммутабельный итератор array_iterator:
Для многомерных массивов все тоже самое:
Иммутабельные указатели
Попробуем слегка обезопасить указатели. В этом разделе рассмотрим обычные указатели (raw pointers), а далее (сильно далее) рассмотрим smart pointers. Для smart pointers будет использоваться SFINAE.
По реализации immutable::pointer скажу сразу, что pointer не удаляет данные, не считает ссылки, а только обеспечивает неизменяемость объекта. (Если переданный указатель изменен или удален из-вне, то это нарушение контракта, которое средствами языка не отследить (стандартными средствами)). В конце-концов, защититься от умышленного вредительства или игры с адресами невозможно. Указатель должен быть корректно инициализирован.
immutable::pointer может работать с указателями на указатели любой степени ссылочности (скажем так).
Кроме вышеперечисленного, immutable::pointer не поддерживает работы со строками в стиле С:
Данный код будет работать не так как ожидается, т.к. immutable::pointer при инкременте возвращает новый immutable::pointer с другим адресом, а в условном выражении будет проверяться результат инкремента, т.е. значение второго символа строки.
Вернемся к реализации. Класс pointer предоставляет общий интерфейс и, в зависимости от того что из себя представляет Tp (указатель на указатель или прото указатель) использует конкретную реализации pointer_impl.
Итого, получается: const T const const *const.
Для простого указателя (который не указывает на другой указатель) реализация следующая:
Для вложенных указателей (указатели на указатели):
Для следующих видов указателей особого смысла не стоит делать специализации:
Иммутабельные smart pointers
Реализация – здесь все просто:
SFINAE
Что это такое и с чем его едят лишний раз объяснять не надо. С помощью SFINAE можно определить наличие в классе методов, типов-членов и т.д, даже наличие перегруженных функций (если задать в выражении testexpr вызов нужной функции с необходимыми параметрами). arg может быть пустым и не участвовать в testexpr. Здесь используется SFINAE с типами и SFINAE с выражениями:
И еще: перегрузку можно разрешить (найти нужную перегруженную функцию) если сигнатуры совпадают, но отличаются квалификатором const [ volatile ] или volatile совместно с SFINAE в три фазы:
1) SFINAE — если есть, то ОК
2) SFINAE + QNonConstOverload, если не получилось, то
3) SFINAE + QConstOverload
В исходниках Qt можно найти интересную и полезную вещь:
Попробуем что получилось:
Операторы
Давате вспомним про операторы! Например, добавим поддержку оператора сложения:
Сначала реализуем оператор сложения вида Immutable + Type:
Т.к. оператор + коммутативен, то Type + Immutable можно реализовать в виде:
И снова, через первую форму реализуем Immutable + Immutable :
Теперь можем работать:
Операторы сравнения должны возвращать значения булевского типа:
SFINAE — это просто
Здравствуйте, коллеги.
Хочу рассказать о SFINAE, интересном и очень полезном (к сожалению*) механизме языка C++, который, однако, может представляться неподготовленному человеку весьма мозгоразрывающим. В действительности принцип его использования достаточно прост и ясен, будучи сформулирован в виде нескольких чётких положений. Эта заметка рассчитана на читателей, обладающих базовыми знаниями о шаблонах в C++ и знакомых, хотя бы шапочно, с C++11.
* Почему к сожалению? Хотя использование SFINAE — интересный и красивый приём, переросший в широко используемую идиому языка, гораздо лучше было бы иметь средства, явно описывающие работу с типами.
Рассмотрим простой пример:
В заключение хочу заострить внимание на том, что потенциал применения этого механизма намного шире, чем проверка свойств типов. Можно использовать его непосредственно по прямому назначению, создавая оптимизированные перегрузки функций и методов: с итераторами произвольного доступа, например, можно позволить себе больше вольностей, чем с последовательными итераторами. А при желании можно и придумать куда более причудливые конструкции, особенно если ваша фамилия Александреску. Отталкиваясь от изложенных в этой заметке базовых принципов, можно создавать мощный, гибкий и надёжный код, умеющий самостоятельно приспосабливаться «на лету» к особенностям используемых типов.
Идиомы Pimpl и Fast Pimpl – указатель на реализацию
Class GeneralSocket <
public :
connect();
private :
UnixSocketImpl socket;
>
//GeneralSocket.cxx
GeneralSocket::connect() <
socket.connectImpl();
>
В данном примере необходимо чтобы описание скрытого класса UnixSocketImpl было известно на этапе компиляции. К тому же ничто не мешает пользователю воспользоваться функциями класса UnixSocketImpl в обход видимого класса GeneralSocket. Теперь попробуем заменить закрытый член видимого класса на указатель и убрать описание скрытого класса UnixSocketImpl из заголовочного файла:
Class GeneralSocket
<
public :
GeneralSocket();
void connect();
private :
UnixSocketImpl * socket;
>
//GeneralSocket.cxx
#include “UnixSocketImpl.h”
GeneralSocket::GeneralSocket() : socket ( new UnixSocketImpl)<>
GeneralSocket() <
delete socket;
socket = 0;
>
void GeneralSocket::connect() <
socket->connectImpl();
>
Нам удалось избавиться от UnixSocketImpl.h в заголовочном файле и перенести его в файл реализации класса GeneralSocket. Теперь пользователь не сможет добраться до конкретной реализации, и будет использовать функционал только через интерфейс класса GeneralSocket.
//GeneralSocket.h
Class GeneralSocket
<
public :
GeneralSocket();
void connect();
private :
static const size_t sizeOfImpl = 42; /* or whatever space needed*/
char socket [sizeOfImpl];
>
//GeneralSocket.cxx
#include “UnixSocketImpl.h”
GeneralSocket::GeneralSocket() : <
assert(sizeOfImpl >= sizeof (UnixSocketImpl));
new (&socket[0]) UnixSocketImpl;
>
GeneralSocket() <
(reinterpret_cast (&socket[0]))->
union <
max_align m;
char socket [sizeOfImpl];
>
class GeneralSocket <
private :
GSimpl * pimpl;
>
//GeneralSocket.cxx
#include “UnixSocketImpl.h”
class FixedAllocator <
public :
static FixedAllocator* Instance();
void * Allocate(size_t);
void Deallocate( void *);
private :
/*Singleton implementation that allocates memory of fixed size*/
>;
void * operator new ( size_t s) <
return FixedAllocator::Instance()->Allocate(s);
>
void operator delete( void * p) <
FixedAllocator::Instance()->Deallocate(p);
>
>;
struct GSimpl : FastPimpl <
/*use UnixSocketImpl here*/
>;
GeneralSocket::GeneralSocket() : pimpl ( new GSimpl)<>
GeneralSocket() <
delete pimpl;
pimpl = 0;
>
Вы можете встретить шаблон Pimpl под другими именами: d-pointer, compiler firewall или даже шаблон Cheshire Cat или непрозрачный указатель.
В его основной форме шаблон выглядит следующим образом:
Таким образом, это может выглядеть так (грубый, старый стиль кода!):
class.h
class.cpp
Два очевидных недостатка этого подхода: нам нужно выделение памяти для хранения закрытой секции. А также основной класс просто перенаправляет вызов метода на закрытую реализацию.
Вышеприведенный код может работать, но мы должны добавить несколько бит, чтобы он работал в реальной жизни.
Больше кода
Мы должны задать несколько вопросов, прежде чем мы сможем написать полный код:
Итак, мы обязательно должны реализовать конструктор копирования (или удалить его, если хотим иметь только перемещаемый тип).
Так что нам нужен механизм конвертации/обертки. Что-то вроде этого:
И теперь, во всех наших методах основного класса, нам следует использовать эту функции-обертку, а не сам указатель.
Улучшенный вариант
Итак, вот улучшенная версия нашего примера кода:
class.h
class.cpp
Сейчас немного лучше.
В приведенном выше коде используется
Вы можете поиграться с полным примером, здесь (в нем также есть несколько приятных вещей для изучения)
cpp_pimpl2.h
cpp_pimpl2.cpp
cpp_pimpl2_client.cpp
Как вы можете видеть, здесь немного кода, который является шаблоном. Вот почему существует несколько подходов к тому, как обернуть эту идиому в отдельный класс утилиты. Давайте посмотрим ниже.
Как отдельный класс
Для примера Herb Sutter в GotW #101: Compilation Firewalls, Part 2 предлагает следующую обертку.
takenFromHerbSutter.h
Тем не менее, вы остаетесь с реализацией конструктора копирования, если это необходимо.
Если вы хотите полномасштабную обертку, взгляните на этот пост PIMPL, Rule of Zero and Scott Meyers Андрея Упадышева.
В этой статье вы можете увидеть очень продвинутую реализацию такого вспомогательного типа:
Fast pimpl
Но имейте в виду, что это всего лишь трюк, возможно, не сработает. Или он может сработать на одной платформе/компиляторе, но не на другой конфигурации.
По моему личному мнению, я не считаю этот подход хорошим. Pimp обычно используется для более крупных классов (возможно, менеджеров, типов в интерфейсах модуля), так что дополнительные затраты не сделают многого.
Мы видели несколько основных частей шаблона pimpl, поэтому теперь мы можем обсудить его сильные и слабые стороны.
За и против
Альтернативы
Как насчет современного C++
Начиная с C++17, у нас нет никаких новых функций, предназначенных для pimpl. С C++11 у нас есть умные указатели, поэтому попробуйте реализовать pimpl с ними, а не с необработанными указателями. Плюс, конечно же, мы получаем множество метапрограммирующих материалов, которые помогают при объявлении типов-оберток для шаблона pimpl.
Но в будущем мы, возможно, захотим рассмотреть два варианта: Модули и оператор точка.
Модули будут играть важную роль в сокращении времени компиляции. Я не игрался с модулями много, но, как я вижу, использование pimpl только для скорости компиляции может стать менее критичным. Конечно, поддержание низкого уровня зависимостей всегда важно.
В принципе, он позволяет перезаписать оператор точки и предоставить гораздо более удобный код для всех типов-прокси.
Кто использует
Я собрал следующие примеры:
Похоже, что шаблон используется где угодно 🙂
Дайте мне знать, если у вас есть другие примеры.
Если вам нужно больше примеров, следуйте этим двум вопросам на stack overflow:
Заключение
Статья написана: Bartek | Понедельник, Январь 8, 2018г.
Рекомендуем хостинг TIMEWEB
Рекомендуемые статьи по этой тематике