АК
Александр Кузьминых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 хостинг.

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

Комментарии

Только авторизованные пользователи могут публиковать комментарии.
Пожалуйста, авторизуйтесь или зарегистрируйтесь
AD

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

  • Результат:50баллов,
  • Очки рейтинга-4
m
  • molni99
  • 26 октября 2024 г. 11:37

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

  • Результат:80баллов,
  • Очки рейтинга4
m
  • molni99
  • 26 октября 2024 г. 11:29

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

  • Результат:20баллов,
  • Очки рейтинга-10
Последние комментарии
ИМ
Игорь Максимов22 ноября 2024 г. 22:51
Django - Урок 017. Кастомизированная страница авторизации на Django Добрый вечер Евгений! Я сделал себе авторизацию аналогичную вашей, все работает, кроме возврата к предидущей странице. Редеректит всегда на главную, хотя в логах сервера вижу запросы на правильн…
Evgenii Legotckoi
Evgenii Legotckoi1 ноября 2024 г. 0:37
Django - Урок 064. Как написать расширение для Python Markdown Добрый день. Да, можно. Либо через такие же плагины, либо с постобработкой через python библиотеку Beautiful Soup
A
ALO1ZE19 октября 2024 г. 18:19
Читалка fb3-файлов на Qt Creator Подскажите как это запустить? Я не шарю в программировании и кодинге. Скачал и установаил Qt, но куча ошибок выдается и не запустить. А очень надо fb3 переконвертировать в html
ИМ
Игорь Максимов5 октября 2024 г. 17:51
Django - Урок 064. Как написать расширение для Python Markdown Приветствую Евгений! У меня вопрос. Можно ли вставлять свои классы в разметку редактора markdown? Допустим имея стандартную разметку: <ul> <li></li> <li></l…
d
dblas55 июля 2024 г. 21:02
QML - Урок 016. База данных SQLite и работа с ней в QML Qt Здравствуйте, возникает такая проблема (я новичок): ApplicationWindow неизвестный элемент. (М300) для TextField и Button аналогично. Могу предположить, что из-за более новой верси…
Сейчас обсуждают на форуме
Evgenii Legotckoi
Evgenii Legotckoi25 июня 2024 г. 1:11
добавить qlineseries в функции Я тут. Работы оень много. Отправил его в бан.
t
tonypeachey115 ноября 2024 г. 17:04
google domain [url=https://google.com/]domain[/url] domain [http://www.example.com link title]
NSProject
NSProject4 июня 2022 г. 13:49
Всё ещё разбираюсь с кешем. В следствии прочтения данной статьи. Я принял для себя решение сделать кеширование свойств менеджера модели LikeDislike. И так как установка evileg_core для меня не была возможна, ибо он писался…
9
9Anonim25 октября 2024 г. 19:10
Машина тьюринга // Начальное состояние 0 0, ,<,1 // Переход в состояние 1 при пустом символе 0,0,>,0 // Остаемся в состоянии 0, двигаясь вправо при встрече 0 0,1,>…

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