АК
Александр Кузьминых15 січня 2018 р. 05: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 міг викликати/посилатися на члени основного класу?
  • що має бути включено до цієї закритої реалізації? все, що закрите?

Перша частина - копіюваність/переміщання відноситься до того, що за допомогою простого вказівника ми можемо лише дрібно скопіювати об'єкт. Звичайно, це відбувається у кожному випадку, коли у вас є вказівник у вашому класі.

Отже, ми обов'язково повинні реалізувати конструктор копіювання (або видалити його, якщо хочемо мати тільки тип, що переміщається).

Як щодо проблеми з 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 пропонує наступну обгортку.

узятийFromHerbSutter.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.

Швидкий прищ

Один очевидний момент про impl у тому, що виділення пам'яті необхідне зберігання закритих частин класу. Якщо вам подобається уникати цього ... і ви дійсно дбаєте про цей розподіл пам'яті ... ви можете спробувати:

  • надати користувальницький аллокатор та використовувати частину фіксованої пам'яті для закритої реалізації
  • або зарезервувати великий блок пам'яті в основному класі та використовувати нове розташування для виділення простору для pimpl.
  • Зверніть увагу, що резервування простору авансом є листковим – що робити, якщо розмір зміниться? І що важливіше - чи є у вас правильне вирівнювання для типу?

Herb Sutter написав про цю ідею тут GotW #28: The Fast Pimpl Idiom

Сучасна версія, що використовує функцію C++11 - aligned_storage описано тут:
Мій улюблений C++ Idiom: Static PIMPL / Fast PIMPL Кая Дітріха або Тип-безпечна реалізація Pimpl без накладних витрат | Можливо, танцювальний блог .

Але майте на увазі, що це лише трюк, можливо, не спрацює. Або він може працювати на одній платформі/компіляторі, але не на іншій конфігурації.

На мою особисту думку, я не вважаю цей підхід добрим. Pimp зазвичай використовується для великих класів (можливо, менеджерів, типів в інтерфейсах модуля), так що додаткові витрати не зроблять багато чого.

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

Плюси і мінуси

для

  • Надає Брандмауер Компіляції : якщо закрита реалізація змінює код клієнта, не потрібно перекомпілювати.
  • Заголовки можуть стати меншими, оскільки типи, згадані тільки в реалізації класу, більше не повинні бути визначені для клієнтського коду.
    Таким чином, загалом це може призвести до кращого часу компіляції.
  • Забезпечує Двійкову Сумісність : дуже важливо для розробників бібліотек. Поки двійковий інтерфейс залишається незмінним, ви можете пов'язати свою програму з іншою версією бібліотеки.
  • Щоб спростити, якщо ви додасте новий віртуальний метод, ABI зміниться, але додавання не віртуальних методів (звичайно без видалення існуючих) не змінює ABI.
  • Смотрите Проблема крихкого бінарного інтерфейсу .
  • Можлива перевага: немає v-таблиці (якщо основний клас містить лише віртуальні методи).
  • Маленький момент: може використовуватися як об'єкт у стеку

Проти

  • Продуктивність - додано один рівень непрямого звернення.
  • Блок пам'яті має бути виділено (або попередньо виділено) для закритої реалізації.
  • Можлива фрагментація пам'яті
  • Складний код вимагає певної дисципліни для підтримки таких класів.
  • Налагодження - ви не бачите деталі відразу, клас розділений

Інші питання

  • Можливість тестування - є думка, що коли ви намагаєтеся протестувати такий клас pimpl, це може викликати проблеми. Але, як правило, ви перевіряєте лише публічний інтерфейс, це не має значення.
  • Не для кожного класу. Цей шаблон часто найкраще підходить для великих класів на «рівні інтерфейсу». Я не думаю, що vector3d із цим шаблоном буде гарною ідеєю...

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

  • Редизайн коду
  • Щоб покращити час збирання:
  • Використовувати заздалегідь скомпільовані заголовкові файли
  • Використовувати кеші збирання
  • Використовувати режим інкрементного складання
  • Абстрактні інтерфейси
  • Не забезпечує сумісність з ABI, але це відмінна альтернатива як технологія порушення залежності
  • Gamasutra - Докладно: PIMPL проти чистих віртуальних інтерфейсів
  • З
    також заснований на абстрактних інтерфейсах, але з деякими більш базовими механізмами.

Як щодо сучасного 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
  • Бібліотека Assimp
  • Експортер
  • Погляньте на цей коментар в 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 хостинг.

Вам це подобається? Поділіться в соціальних мережах!

Коментарі

Only authorized users can post comments.
Please, Log in or Sign up
AD

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

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

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

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

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

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

Слідкуйте за нами в соціальних мережах