АК
Александр Кузьминых15 января 2018 г. 5:18

Шаблон программирования Pimpl - то, что вам следует знать

Основы

Вы можете встретить шаблон Pimpl под другими именами: d-pointer, compiler firewall или даже шаблон Cheshire Cat или непрозрачный указатель.

В его основной форме шаблон выглядит следующим образом:

  • В классе мы перемещаем все закрытые члены в новый объявленный тип, например, в класс PrivateImpl .
  • Объявляем PrivateImpl в заголовочном файле основного класса.
  • В соответствующем файле cpp объявляем класс PrivateImpl и определяем его.
  • Теперь, если вы измените закрытую реализацию, код клиента не будет перекомпилирован (поскольку интерфейс не изменился).

Таким образом, это может выглядеть так (грубый, старый стиль кода!):

class.h

class MyClassImpl;
class MyClass {
    // ...
    void Foo();
private:    
    MyClassImpl* m_pImpl; // Предупреждение!!! 
                          // raw-указатель! :)
};

class.cpp

class MyClassImpl
{
public:
    void DoStuff() { /*...*/ }
};

MyClass::MyClass () 
: m_pImpl(new MyClassImpl()) 
{ }

MyClass::~MyClass () { delete m_pImpl; }

void MyClass ::DoSth() {
    m_pImpl->DoSth();
}

Эх ... уродливые raw-указатели!

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

Конечно, в современном C ++ рекомендуется использовать unique_ptr , а не raw-указатели.

Два очевидных недостатка этого подхода: нам нужно выделение памяти для хранения закрытой секции. А также основной класс просто перенаправляет вызов метода на закрытую реализацию.

Хорошо ... но это все ... верно? Не так просто!

Вышеприведенный код может работать, но мы должны добавить несколько бит, чтобы он работал в реальной жизни.


Больше кода

Мы должны задать несколько вопросов, прежде чем мы сможем написать полный код:

  • является ли ваш класс копируемым или только перемещаемым?
  • как принудительно использовать const для методов в этой закрытой реализации?
  • вам нужен «обратный» указатель, чтобы класс impl мог вызывать/ссылаться на члены основного класса?
  • что должно быть включено в эту закрытую реализацию? все, что является закрытым?

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

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

Как насчет проблемы с const ? Можете ли вы отловить её в основном примере?

Если вы объявляете метод const , вы не можете изменять членов объекта. Другими словами, они становятся const . Но это проблема для нашего m_pImpl , который является указателем. В методе const этот указатель также станет const, что означает, что мы не можем присвоить ему другое значение ... но ... мы можем с радостью вызвать все методы этого базового закрытого класса (а не только константы)!

Так что нам нужен механизм конвертации/обертки. Что-то вроде этого:

const MyClassImpl* Pimpl() const { return m_pImpl; }
MyClassImpl* Pimpl() { return m_pImpl; }

И теперь, во всех наших методах основного класса, нам следует использовать эту функции-обертку, а не сам указатель.

До сих пор я не упоминал об этом «обратном» указателе («q-указатель» в терминологии Qt). Ответ связан с последним пунктом - что мы должны вводить в закрытую реализацию - только закрытые поля? Или, может быть, даже закрытые функции?

Код с основами не покажет эти практические проблемы. Но в реальном приложении класс может содержать множество методов и полей. Я видел примеры, когда вся закрытая часть (с помощью методов) переходила в класс pimpl . Тем не менее, иногда класс pimpl должен вызывать «реальный» метод основного класса, поэтому нам нужно предоставить этот «обратный» указатель. Это можно сделать при конструировании, просто передайте указатель на this .

Улучшенный вариант

Итак, вот улучшенная версия нашего примера кода:

class.h

class MyClassImpl;
class MyClass
{
public:
    explicit MyClass();
    ~MyClass(); 

    // movable:
    MyClass(MyClass && rhs) noexcept;   
    MyClass& operator=(MyClass && rhs) noexcept;

    // and copyable
    MyClass(const MyClass& rhs);
    MyClass& operator=(const MyClass& rhs);

    void DoSth();
    void DoConst() const;

private:
    const MyClassImpl* Pimpl() const { return m_pImpl.get(); }
    MyClassImpl* Pimpl() { return m_pImpl.get(); }

    std::unique_ptr<MyClassImpl> m_pImpl;
};

class.cpp

class MyClassImpl
{
public:
    ~MyClassImpl() = default;

    void DoSth() { }
    void DoConst() const { }
};

MyClass::MyClass() : m_pImpl(new MyClassImpl()) 
{

}

MyClass::~MyClass() = default;
MyClass::MyClass(MyClass &&) noexcept = default;
MyClass& MyClass::operator=(MyClass &&) noexcept = default;

MyClass::MyClass(const MyClass& rhs)
    : m_pImpl(new MyClassImpl(*rhs.m_pImpl))
{}

MyClass& MyClass::operator=(const MyClass& rhs) {
    if (this != &rhs) 
        m_pImpl.reset(new MyClassImpl(*rhs.m_pImpl));

    return *this;
}

void MyClass::DoSth()
{
    Pimpl()->DoSth();
}

void MyClass::DoConst() const
{
    Pimpl()->DoConst();
}

Сейчас немного лучше.

В приведенном выше коде используется

  • unique_ptr - но посмотрите, что деструктор для основного класса должен быть определен в файле cpp. В противном случае компилятор будет жаловаться на отсутствие удаляемого типа...
  • Класс является перемещаемым и копируемым, так как определены четыре метода
  • Чтобы быть в безопасности с методами const, все прокси-методы основного класса используют метод Pimpl() для получения правильного типа указателя.

Взгляните на этот блок Pimp My Pimpl — Reloaded , чтобы получить больше информации о Pimpl.

Вы можете поиграться с полным примером, здесь (в нем также есть несколько приятных вещей для изучения)

cpp_pimpl2.h

#include <memory>

class MyClassImpl;
class MyClass
{
public:
    explicit MyClass();
    ~MyClass(); 

    // movable:
    MyClass(MyClass && rhs) noexcept;   
    MyClass& operator=(MyClass && rhs) noexcept;

    // and copyable:
    MyClass(const MyClass& rhs);
    MyClass& operator=(const MyClass& rhs);

    void DoSth();
    void DoConst() const;

private:
    // const access:
    const MyClassImpl* Pimpl() const { return m_pImpl.get(); }
    MyClassImpl* Pimpl() { return m_pImpl.get(); }

    std::unique_ptr<MyClassImpl> m_pImpl;
};

cpp_pimpl2.cpp

#include "cpp_pimpl2.h"
#include <iostream>

void* operator new(size_t count) {
    std::cout << "allocating " << count << " bytes\n";
    return malloc(count);
}

void operator delete(void* ptr) noexcept {
    std::cout << "global op delete called\n";
    free(ptr);
}

class MyClassImpl
{
public:
    MyClassImpl(MyClass* pBackPtr) : m_pMainClass(pBackPtr) { }
    ~MyClassImpl() = default;

    void DoSth() { std::cout << "Val (incremented): " << ++m_val << "\n";}

    // try to uncomment (or comment 'const' for the method)
    void DoConst() const { 
        std::cout << "Val: " << /*++*/m_val << "\n"; 
    } 

private:
    int m_val {0};
    MyClass* m_pMainClass {nullptr}; // back pointer
};

MyClass::MyClass() : m_pImpl(new MyClassImpl(this)) 
{
    std::cout <<  __PRETTY_FUNCTION__ << "\n";
}

MyClass::~MyClass() { std::cout <<  __PRETTY_FUNCTION__ << "\n"; }
MyClass::MyClass(MyClass &&) noexcept = default;
MyClass& MyClass::operator=(MyClass &&) noexcept = default;

MyClass::MyClass(const MyClass& rhs)
    : m_pImpl(new MyClassImpl(*rhs.m_pImpl))
{}

MyClass& MyClass::operator=(const MyClass& rhs) {
    if (this != &rhs) 
        m_pImpl.reset(new MyClassImpl(*rhs.m_pImpl));

    return *this;
}

void MyClass::DoSth()
{
    Pimpl()->DoSth();
}

void MyClass::DoConst() const
{
    Pimpl()->DoConst();
}

cpp_pimpl2_client.cpp

#include "cpp_pimpl2.h"
#include <iostream>
#include <vector>

int main()
{
    MyClass myObject;
    myObject.DoSth();

    const MyClass secondObject;
    secondObject.DoConst();
}

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

Как отдельный класс

Для примера Herb Sutter в GotW #101: Compilation Firewalls, Part 2 предлагает следующую обертку.

takenFromHerbSutter.h

template<typename T>
class pimpl {
private:
    std::unique_ptr<T> m;
public:
    pimpl();
    template<typename ...Args> pimpl( Args&& ... );
    ~pimpl();
    T* operator->();
    T& operator*();
};

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

Если вы хотите полномасштабную обертку, взгляните на этот пост PIMPL, Rule of Zero and Scott Meyers Андрея Упадышева.

В этой статье вы можете увидеть очень продвинутую реализацию такого вспомогательного типа:

SPIMPL (Smart Pointer to IMPLementation) -  состоящая только из заголовочных файлов небольшая библиотека C++11, с целью упростить реализацию идиомы PIMPL

Внутри библиотеки вы можете найти два типа: 1) spimpl::unique_impl_ptr - для только перемещаемой обертки pimpl и 2) spimpl::impl_ptr для перемещаемой и копируемой обертки pimpl.

Fast pimpl

Один очевидный момент об impl в том, что выделение памяти необходимо для хранения закрытых частей класса. Если вам нравится избегать этого ... и вы действительно заботитесь об этом распределении памяти ... вы можете попробовать:

  • предоставить пользовательский аллокатор и использовать часть фиксированной памяти для закрытой реализации
  • или зарезервировать большой блок памяти в основном классе и использовать новое размещение для выделения пространства для pimpl.
  • Обратите внимание, что резервирование пространства авансом является слоеным - что делать, если размер изменится? И что более важно - есть ди у вас правильное выравнивание для типа?

Herb Sutter написал об этой идее здесь GotW #28: The Fast Pimpl Idiom .

Современная версия, использующая функцию C++11 - aligned_storage описано здесь:
My Favourite C++ Idiom: Static PIMPL / Fast PIMPL by Kai Dietrich or Type-safe Pimpl implementation without overhead | Probably Dance blog .

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

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

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

За и против

За

  • Предоставляет Брандмауэр Компиляции : если закрытая реализация изменяет код клиента, не нужно перекомпилировать.
  • Заголовки могут стать меньше, так как типы, упомянутые только в реализации класса, больше не должны быть определены для клиентского кода.
  • Таким образом, в целом, это может привести к лучшему времени компиляции.
  • Обеспечивает Двоичную Совместимость : очень важно для разработчиков библиотек. Пока двоичный интерфейс остается прежним, вы можете связать свое приложение с другой версией библиотеки.
  • Чтобы упростить, если вы добавите новый виртуальный метод, ABI изменится, но добавление не виртуальных методов (конечно, без удаления существующих) не изменяет ABI.
  • Смотрите Fragile Binary Interface Problem .
  • Возможное преимущество: нет v-таблицы (если основной класс содержит только не виртуальные методы).
  • Маленький момент: может использоваться как объект в стеке

Против

  • Производительность - добавлен один уровень косвенного обращения.
  • Блок памяти должен быть выделен (или предварительно выделен) для закрытой реализации.
  • Возможная фрагментация памяти
  • Сложный код и требует определенной дисциплины для поддержания таких классов.
  • Отладка - вы не видите детали сразу, класс разделен

Иные вопросы

  • Возможность тестирования - есть мнение, что, когда вы пытаетесь протестировать такой класс pimpl, это может вызвать проблемы. Но, как правило, вы проверяете только публичный интерфейс, это не имеет значения.
  • Не для каждого класса. Этот шаблон часто лучше подходит для больших классов на «уровне интерфейса». Я не думаю, что vector3d с этим шаблоном будет хорошей идеей...

Альтернативы

  • Редизайн кода
  • Чтобы улучшить время сборки:
  • Использовать предварительно скомпилированные заголовочные файлы
  • Использовать кэши сборки
  • Использовать режим инкрементной сборки
  • Абстрактные интерфейсы
  • Не обеспечивает совместимость с ABI, но это отличная альтернатива в качестве технологии нарушения зависимости
  • Gamasutra - In-depth: PIMPL vs pure virtual interfaces
  • COM
  • также основанный на абстрактных интерфейсах, но с некоторыми более базовыми механизмами.

Как насчет современного C++

Начиная с C++17, у нас нет никаких новых функций, предназначенных для pimpl. С C++11 у нас есть умные указатели, поэтому попробуйте реализовать pimpl с ними, а не с необработанными указателями. Плюс, конечно же, мы получаем множество метапрограммирующих материалов, которые помогают при объявлении типов-оберток для шаблона pimpl.

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

Модули будут играть важную роль в сокращении времени компиляции. Я не игрался с модулями много, но, как я вижу, использование pimpl только для скорости компиляции может стать менее критичным. Конечно, поддержание низкого уровня зависимостей всегда важно.

Еще одной особенностью, которая может стать удобной, является оператор точка, разработанная Бьярне Страуступом и Габриэлем Досом Рейсом. PDF - N4477 - не сделан в C++17, но, возможно, мы увидим его в C++20?

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

Кто использует

Я собрал следующие примеры:

  • QT:
  • Это, вероятно, наиболее яркие примеры (которые вы можете найти в открытом доступе), в которых были широко использованы закрытые реализации.
  • Есть даже хорошая вводная статья, в которой обсуждаются d-указатели (так они называют pimpl): Pointer - Qt Wiki
  • QT также показывает, как использовать pimpl с наследованием. Теоретически вам нужен отдельный pimpl для каждого производного класса, но QT использует только один указатель.
  • OpenSceneGraph
  • Framebuffer object
  • Assimp library
  • Exporter
  • Взгляните на этот комментарий в assimp.hpp :)
// Священный материал, только для членов высшего совета джедаев.
class ImporterPimpl;

// ...

// Просто потому, что мы не хотим, чтобы вы знали, как мы взламываем.
ImporterPimpl* pimpl;

Похоже, что шаблон используется где угодно :)

Дайте мне знать, если у вас есть другие примеры.

Если вам нужно больше примеров, следуйте этим двум вопросам на stack overflow:

Заключение

Pimpl выглядит просто ... но, как обычно, на C++ вещи на практике не так просты :)

Основные моменты:

  • Pimpl обеспечивает совместимость с ABI и уменьшает зависимости компиляции.
  • Начиная с C++11, вы должны использовать unique_ptr (или даже shared_ptr) для реализации шаблона.
  • Чтобы заставить шаблон работать, решите, должен ли ваш основной класс быть копируемым или просто перемещаемым.
  • Позаботьтесь о методах const, чтобы закрытая реализация соблюдала их.
  • Если для закрытой реализации требуется доступ к основным членам класса, тогда необходим «обратный указатель».
  • Возможны некоторые оптимизации (чтобы избежать раздельного распределения памяти), но это может оказаться сложной задачей.
  • Существует много применений шаблона в проектах с открытым исходным кодом, QT активно его использует (с наследованием и обратным указателем)

Статья написана:  Bartek | Понедельник, Январь 8, 2018г.

Рекомендуем хостинг TIMEWEB
Рекомендуем хостинг TIMEWEB
Стабильный хостинг, на котором располагается социальная сеть EVILEG. Для проектов на Django рекомендуем VDS хостинг.

Вам это нравится? Поделитесь в социальных сетях!

Комментарии

Только авторизованные пользователи могут публиковать комментарии.
Пожалуйста, авторизуйтесь или зарегистрируйтесь
г
  • ги
  • 23 апреля 2024 г. 15:51

C++ - Тест 005. Структуры и Классы

  • Результат:41баллов,
  • Очки рейтинга-8
l
  • laei
  • 23 апреля 2024 г. 9:19

C++ - Тест 004. Указатели, Массивы и Циклы

  • Результат:10баллов,
  • Очки рейтинга-10
l
  • laei
  • 23 апреля 2024 г. 9:17

C++ - Тест 003. Условия и циклы

  • Результат:50баллов,
  • Очки рейтинга-4
Последние комментарии
k
kmssr8 февраля 2024 г. 18:43
Qt Linux - Урок 001. Автозапуск Qt приложения под Linux как сделать автозапуск для флэтпака, который не даёт создавать файлы в ~/.config - вот это вопрос ))
АК
Анатолий Кононенко5 февраля 2024 г. 1:50
Qt WinAPI - Урок 007. Работаем с ICMP Ping в Qt Без строки #include <QRegularExpressionValidator> в заголовочном файле не работает валидатор.
EVA
EVA25 декабря 2023 г. 10:30
Boost - статическая линковка в CMake проекте под Windows Ошибка LNK1104 часто возникает, когда компоновщик не может найти или открыть файл библиотеки. В вашем случае, это файл libboost_locale-vc142-mt-gd-x64-1_74.lib из библиотеки Boost для C+…
J
JonnyJo25 декабря 2023 г. 8:38
Boost - статическая линковка в CMake проекте под Windows Сделал всё по-как у вас, но выдаёт ошибку [build] LINK : fatal error LNK1104: не удается открыть файл "libboost_locale-vc142-mt-gd-x64-1_74.lib" Хоть убей, не могу понять в чём дел…
G
Gvozdik18 декабря 2023 г. 21:01
Qt/C++ - Урок 056. Подключение библиотеки Boost в Qt для компиляторов MinGW и MSVC Для решения твой проблемы добавь в файл .pro строчку "LIBS += -lws2_32" она решит проблему , лично мне помогло.
Сейчас обсуждают на форуме
G
Gar22 апреля 2024 г. 5:46
Clipboard Как скопировать окно целиком в clipb?
DA
Dr Gangil Academics20 апреля 2024 г. 7:45
Unlock Your Aesthetic Potential: Explore MSC in Facial Aesthetics and Cosmetology in India Embark on a transformative journey with an msc in facial aesthetics and cosmetology in india . Delve into the intricate world of beauty and rejuvenation, guided by expert faculty and …
a
a_vlasov14 апреля 2024 г. 6:41
Мобильное приложение на C++Qt и бэкенд к нему на Django Rest Framework Евгений, добрый день! Такой вопрос. Верно ли следующее утверждение: Любое Android-приложение, написанное на Java/Kotlin чисто теоретически (пусть и с большими трудностями) можно написать и на C+…
Павел Дорофеев
Павел Дорофеев14 апреля 2024 г. 2:35
QTableWidget с 2 заголовками Вот тут есть кастомный QTableView с многорядностью проект поддерживается, обращайтесь
f
fastrex4 апреля 2024 г. 4:47
Вернуть старое поведение QComboBox, не менять индекс при resetModel Добрый день! У нас много проектов в которых используется QComboBox, в версии 5.5.1, когда модель испускает сигнал resetModel, currentIndex не менялся. В версии 5.15 при resetModel происходит try…

Следите за нами в социальных сетях