Шаблон проектирования PImpl (Pointer to Implementation), также известный как Opaque Pointer, появился как средство разделения интерфейса и реализации класса в C++. Это особенно важно для обеспечения совместимости бинарных интерфейсов (ABI), ускорения компиляции и скрытия деталей реализации от пользователя класса.
История вопроса.
В большом числе C++ проектов приходится модифицировать реализации классов, не изменяя публичный интерфейс и не перекомпилируя клиентов этих классов. Проблема заключается в том, что любые изменения в заголовочных файлах требуют пересборки всех зависимых модулей, что может быть крайне затратно на больших кодовых базах. PImpl позволяет минимизировать пересборку и обеспечивает лучшую инкапсуляцию.
Проблема.
Стандартный способ определения класса с приватными членами в заголовочном файле требует знания всех этих членов при компиляции. При расширении или изменении их приходится пересобирать все файлы, которые включают этот заголовок. Кроме того, это раскрывает детали реализации/структуры клиента, возможно отрицательно влияя на безопасность и архитектурную целостность.
Решение.
PImpl реализует скрытие реализации за счёт использования указателя на forward-декларируемую структуру-реализацию (Impl struct/class), определяемую в cpp. Это позволяет менять реализацию без затрагивания интерфейса.
Пример кода:
// Widget.h class Widget { public: Widget(); ~Widget(); void doSomething(); private: struct Impl; Impl* pimpl; // opaque pointer }; // Widget.cpp #include "Widget.h" struct Widget::Impl { int secret; }; Widget::Widget() : pimpl(new Impl{42}) {} // secrecy inside Widget::~Widget() { delete pimpl; } void Widget::doSomething() { pimpl->secret += 1; }
Ключевые особенности:
Можно ли использовать std::unique_ptr вместо сырого указателя в PImpl?
Да, современный и безопасный подход — использовать std::unique_ptr (или std::shared_ptr, если требуется разделение владения). Это позволяет корректно управлять памятью и не писать явно деструктор/оператор копирования для сырого указателя:
private: std::unique_ptr<Impl> pimpl;
Можно ли сделать класс с PImpl перемещаемым, но не копируемым?
Да, если предоставить move-конструктор/оператор, но удалить копирующий. Например:
Widget(Widget&&) noexcept = default; Widget& operator=(Widget&&) noexcept = default; Widget(const Widget&) = delete; Widget& operator=(const Widget&) = delete;
Появляется ли накладной расход на производительность при использовании PImpl?
Да, за счёт разыменования указателя и дополнительного динамического выделения памяти (heap allocation). Для критично-производительных структур это может быть существенным минусом.
Большая компания внедрила PImpl для всех классов подряд, в том числе для простых структур данных, что привело к существенному замедлению простых операций из-за постоянного разыменования указателей.
Плюсы:
Минусы:
В проекте с долгоживущей библиотекой пользовательского интерфейса PImpl применили только для сложных виджетов с часто изменяющейся внутренностью, сохраняя стабильный ABI для сторонних клиентов.
Плюсы:
Минусы: