© EVILEG 2015-2018
Рекомендует хостинг
TIMEWEB

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

C++, 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 с этим шаблоном будет хорошей идеей...

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

  • Редизайн кода
  • Чтобы улучшить время сборки:
    • Использовать предварительно скомпилированные заголовочные файлы
      • Использовать кэши сборки
      • Использовать режим инкрементной сборки
  • Абстрактные интерфейсы
  • 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
  • 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г.

Комментарии

Комментарии

Только авторизованные пользователи могут оставлять комментарии.
Пожалуйста, Авторизуйтесь или Зарегистрируйтесь
15 октября 2018 г. 21:36
Allyans .

C++ - Тест 001. Первая программа и типы данных

  • Результат 60баллов,
  • Очки рейтинга-1
15 октября 2018 г. 11:25
Екатерина Самойлова

C++ - Тест 002. Константы

  • Результат 33баллов,
  • Очки рейтинга-10
15 октября 2018 г. 11:17
Екатерина Самойлова

C++ - Тест 006. Перечисления

  • Результат 80баллов,
  • Очки рейтинга4
Последние комментарии
17 октября 2018 г. 8:43
pasagir

Qt/C++ - Урок 006. QSqlQueryModel - Таблицы в Qt с помощью SQL-запросов

Не получается bool DataBase::insertDataIntoDB(QVariantList data){ QSqlQuery query(db); QString str; qDebug()<<"InsertInsertInsertInsertInsert"<<QThread::curre...
17 октября 2018 г. 7:09
Евгений Легоцкой

Qt/C++ - Урок 006. QSqlQueryModel - Таблицы в Qt с помощью SQL-запросов

Попробуйте передать инстанс базы данных в конструктор QSqlQuery QSqlQuery q(db);
16 октября 2018 г. 16:14
pasagir

Qt/C++ - Урок 006. QSqlQueryModel - Таблицы в Qt с помощью SQL-запросов

В Qt 5.11. при попытке вставить в БД запись выдает ошибку QSqlQuery::prepare: database not openQSqlDatabasePrivate::database: requested database does not belong to the calling thread. ...
10 октября 2018 г. 9:50
Евгений Легоцкой

Qt/C++ - Урок 083. Создание динамической библиотеки и подключение её в другой проект

Если и начинать писать о плагинах, то нужно тогда с Qt Creator начинать, там наверняка будет одинаковый принцип, но по Qt Creator хотя бы информация есть.
Сейчас обсуждают на форуме
17 октября 2018 г. 16:33
Allyans .

Работа с WinAPI в QT(изменение title bar)

Здравствуйте. Я хочу в своей программе изменить цвет title bar. Так как в qt нет не каких функций связаных с этим я искал в интернете ответ на мой вопрос и там советовали функцию SetSysColors(...
17 октября 2018 г. 13:39
Михаиллл

Настройка Qt Creator для Android

Здравствуйте. У меня установлены SDK, NDK. Но для компилятора не хватает arm-linux-android-elf-64bit, 86-linux-android-elf-64bit . Скажите пожалуйста как это исправить?
15 октября 2018 г. 12:45
Allyans .

QGraphicsItem change color

Хорошо)
11 октября 2018 г. 10:13
Arrow

Работа с WebView в QML

Нашел в чем проблема. Пишу на случай если кому-то попадется такое же счастье с WebView как и мне. Проблема как оказалась с Debug версией, так как в Release и Profile все работает (...
10 октября 2018 г. 12:49
Виталий Антипов

Кто что делает на Qt?

Работаем по локальной сети. Файл базы, схемы и фото лежат на сервере. Чтобы не было проблем при одновременной работе с одним файлом, все запросы обернул в транзакции, как указано в документаци...
Присоединяйтесь к нам в социальных сетях