Действительно ли идиома pImpl используется на практике?

165

Я читаю книгу «Исключительный C ++» Херба Саттера, и в этой книге я узнал об идиоме pImpl. По сути, идея состоит в том, чтобы создать структуру для privateобъектов classи динамически распределить их, чтобы уменьшить время компиляции (а также лучше скрыть частные реализации).

Например:

class X
{
private:
  C c;
  D d;  
} ;

может быть изменено на:

class X
{
private:
  struct XImpl;
  XImpl* pImpl;       
};

и в CPP определение:

struct X::XImpl
{
  C c;
  D d;
};

Это кажется довольно интересным, но я никогда не видел такого подхода раньше, ни в компаниях, где я работал, ни в проектах с открытым исходным кодом, где я видел исходный код. Итак, мне интересно, эта техника действительно используется на практике?

Я должен использовать это везде или с осторожностью? И рекомендуется ли этот метод использовать во встроенных системах (где производительность очень важна)?

Ренан Грейнерт
источник
По сути, это то же самое, что решить, что X является (абстрактным) интерфейсом, а Ximpl является реализацией? struct XImpl : public X, Это кажется мне более естественным. Есть ли какая-то другая проблема, которую я пропустил?
Аарон МакДейд
@AaronMcDaid: Это похоже, но имеет те преимущества, что (а) функции-члены не обязательно должны быть виртуальными, и (б) вам не нужна фабрика или определение класса реализации, чтобы создать его экземпляр.
Майк Сеймур
2
@AaronMcDaid Идиома pimpl позволяет избежать вызовов виртуальных функций. Это также немного больше C ++ - иш (для некоторой концепции C ++ - иш); Вы вызываете конструкторы, а не фабричные функции. Я использовал оба, в зависимости от того, что в существующей кодовой базе - идиома pimpl (первоначально называемая идиомой Чеширского кота и предшествующая описанию Хербом по крайней мере 5 лет), кажется, имеет более длинную историю и более широко используется в C ++, но в остальном оба работают.
Джеймс Канз
30
В C ++ pimpl должен быть реализован с, const unique_ptr<XImpl>а не XImpl*.
Нил Дж
1
«никогда раньше не видел такого подхода ни в компаниях, где я работал, ни в проектах с открытым исходным кодом». Qt вряд ли когда-либо использует это НЕ.
ManuelSchneid3r

Ответы:

132

Итак, мне интересно, эта техника действительно используется на практике? Я должен использовать это везде или с осторожностью?

Конечно это используется. Я использую это в своем проекте, почти в каждом классе.


Причины использования идиомы PIMPL:

Бинарная совместимость

Когда вы разрабатываете библиотеку, вы можете добавлять / изменять поля, XImplне нарушая бинарную совместимость с вашим клиентом (что будет означать сбои!). Поскольку двоичный макет Xкласса не изменяется при добавлении новых полей в Ximplкласс, безопасно добавлять новые функции в библиотеку при обновлении второстепенных версий.

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

Скрытие данных

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

Время компиляции

Время компиляции уменьшается, поскольку Xпри добавлении / удалении полей и / или методов к XImplклассу необходимо перестраивать только исходный файл (файл реализации), который сопоставляется с добавлением приватных полей / методов в стандартной технике). На практике это обычная операция.

При использовании стандартного метода заголовка / реализации (без PIMPL) при добавлении нового поля Xкаждый клиент, который когда-либо выделяет X(либо в стеке, либо в куче), должен быть перекомпилирован, поскольку он должен регулировать размер выделения. Хорошо, каждый клиент, который никогда не выделяет X, также должен быть перекомпилирован, но это просто накладные расходы (результирующий код на стороне клиента будет таким же).

Более того, со стандартным разделением заголовок / реализация XClient1.cppнеобходимо перекомпилировать, даже если частный метод X::foo()был добавлен Xи X.hизменен, даже если он XClient1.cppне может вызывать этот метод по причинам инкапсуляции! Как и выше, это просто накладные расходы и связано с тем, как работают реальные системы сборки C ++.

Конечно, перекомпиляция не нужна, когда вы просто изменяете реализацию методов (потому что вы не трогаете заголовок), но это на уровне стандартной техники заголовка / реализации.


Рекомендуется ли использовать эту технику во встроенных системах (где производительность очень важна)?

Это зависит от того, насколько сильна ваша цель. Тем не менее, единственный ответ на этот вопрос: измерить и оценить, что вы получаете и теряете. Кроме того, примите во внимание, что если вы не публикуете библиотеку, предназначенную для использования во встроенных системах вашими клиентами, применяется только преимущество времени компиляции!

BЈовић
источник
16
+1, потому что он широко используется в компании, в которой я работаю, и по тем же причинам.
Бенуа
9
также бинарная совместимость
Амброз Бизжак
9
В библиотеке Qt этот метод также используется в ситуациях умного указателя. Поэтому QString хранит свое содержимое как неизменяемый класс внутри. Когда открытый класс «копируется», копируется указатель закрытого члена вместо всего закрытого класса. Эти частные классы также используют интеллектуальные указатели, так что вы в основном получаете сборку мусора с большинством классов, в дополнение к значительно улучшенной производительности благодаря копированию указателей вместо полного копирования классов
Тимоти Болдридж
8
Более того, с помощью языка pimpl Qt может поддерживать прямую и обратную двоичную совместимость в одной основной версии (в большинстве случаев). ИМО это, безусловно, самая значительная причина для его использования.
whitequark
1
Это также полезно для реализации кода для платформы, так как вы можете сохранить тот же API.
документ
49

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

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

Преимущества, которые он может дать вам:

  • помогает в поддержании двоичной совместимости разделяемых библиотек
  • скрытие определенных внутренних деталей
  • уменьшение циклов перекомпиляции

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

Возможные недостатки (также здесь, в зависимости от реализации и являются ли они реальными недостатками для вас):

  • Увеличение использования памяти из-за большего количества выделений, чем с наивным вариантом
  • увеличенные усилия по обслуживанию (вы должны написать хотя бы функции пересылки)
  • потеря производительности (возможно, компилятор не сможет встроить что-либо, как в случае с наивной реализацией вашего класса)

Так что тщательно оцените все и оцените это для себя. Для меня почти всегда получается, что использование идиомы pimpl не стоит усилий. Есть только один случай, когда я лично им пользуюсь (или хотя бы чем-то похожим):

Моя оболочка C ++ для statвызова Linux . Здесь структура из заголовка C может отличаться в зависимости от того, что #definesустановлено. И так как мой заголовок оболочки не может контролировать их все, я только #include <sys/stat.h>в своем .cxxфайле и избегаю этих проблем.

PlasmaHH
источник
2
Его почти всегда следует использовать для системных интерфейсов, чтобы сделать систему кода интерфейса независимой. Мой Fileкласс (который предоставляет большую часть информации stat, возвращаемой в Unix) использует один и тот же интерфейс, например, в Windows и Unix.
Джеймс Канзе
5
@JamesKanze: Даже там я лично сначала немного посижу и подумаю, может быть, недостаточно иметь несколько #ifdefсекунд, чтобы сделать обертку максимально тонкой. Но у всех разные цели, главное - подумать об этом, а не слепо следовать чему-то.
PlasmaHH
31

Согласитесь со всеми остальными в отношении товаров, но позвольте мне показать ограничение: не очень хорошо работает с шаблонами .

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

Вы по-прежнему можете ссылаться на шаблонные подклассы, но, поскольку вы должны включить их все, все преимущества «разъединения реализации» при компиляции (избегая включения всего кода, специфичного для платформ, везде, сокращая компиляцию) теряются.

Является хорошей парадигмой для классического ООП (на основе наследования), но не для общего программирования (на основе специализации).

Эмилио Гаравалья
источник
4
Вы должны быть более точными: нет абсолютно никаких проблем при использовании классов PIMPL в качестве аргументов типа шаблона. Только если сам класс реализации должен быть параметризован для аргументов шаблона внешнего класса, он больше не может быть скрыт от заголовка интерфейса, даже если это все еще закрытый класс. Если вы можете удалить аргумент шаблона, вы все равно можете сделать «правильный» PIMPL. При удалении типа вы также можете выполнить PIMPL в базовом не шаблонном классе, а затем получить из него класс шаблона.
Восстановите Монику
22

Другие люди уже предоставили технические плюсы и минусы, но я думаю, стоит отметить следующее:

Прежде всего, не будьте догматичными. Если pImpl подходит для вашей ситуации, используйте его - не используйте его только потому, что «он лучше ОО, поскольку он действительно скрывает реализацию» и т. Д. Цитирование часто задаваемых вопросов по C ++:

инкапсуляция для кода, а не людей ( источник )

Просто чтобы дать вам пример программного обеспечения с открытым исходным кодом, где оно используется и почему: OpenThreads, библиотека потоков, используемая OpenSceneGraph . Основная идея состоит в том, чтобы удалить из заголовка (например <Thread.h>) весь специфичный для платформы код, потому что внутренние переменные состояния (например, дескрипторы потоков) отличаются от платформы к платформе. Таким образом, вы можете скомпилировать код для вашей библиотеки без знания особенностей других платформ, потому что все скрыто.

азалия
источник
12

Я бы в основном рассмотрел PIMPL для классов, которые могут быть использованы в качестве API другими модулями. Это имеет много преимуществ, поскольку перекомпиляция изменений, внесенных в реализацию PIMPL, не влияет на остальную часть проекта. Кроме того, для классов API они обеспечивают двоичную совместимость (изменения в реализации модуля не влияют на клиентов этих модулей, их не нужно перекомпилировать, поскольку новая реализация имеет тот же двоичный интерфейс - интерфейс, предоставляемый PIMPL).

Что касается использования PIMPL для каждого класса, я бы подумал об осторожности, потому что все эти преимущества обходятся дорого: для доступа к методам реализации необходим дополнительный уровень косвенности.

Ghita
источник
«Для доступа к методам реализации необходим дополнительный уровень косвенности». Это?
xaxxon
@ xaxxon да, это так. pimpl медленнее, если методы низкого уровня. например, никогда не используйте его для вещей, которые живут в узком кругу.
Эрик Аронесты
@xaxxon Я бы сказал, что в общем случае требуется дополнительный уровень. Если встраивание выполняется, то нет. Но встраивание не было бы вариантом в коде, скомпилированном в другой DLL.
Гита
5

Я думаю, что это один из самых фундаментальных инструментов для развязки.

Я использовал pimpl (и многие другие идиомы из Exceptional C ++) во встроенном проекте (SetTopBox).

Особая цель этого idoim в нашем проекте состояла в том, чтобы скрыть типы, используемые классом XImpl. В частности, мы использовали его, чтобы скрыть подробности реализаций для разных аппаратных средств, в которые будут вставляться разные заголовки. У нас были разные реализации классов XImpl для одной платформы и разные для другой. Планировка класса X осталась прежней независимо от платформы.

user377178
источник
4

Раньше я часто использовал эту технику, но потом отошел от нее.

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

Преимущества pImpl:

  1. Предполагая, что есть только одна реализация этого интерфейса, это более ясно, не используя абстрактный класс / конкретную реализацию

  2. Если у вас есть набор классов (модуль), такой, что несколько классов обращаются к одному и тому же «impl», но пользователи модуля будут использовать только «открытые» классы.

  3. Нет V-таблицы, если предполагается, что это плохо.

Недостатки, которые я нашел в pImpl (где абстрактный интерфейс работает лучше)

  1. Хотя у вас может быть только одна «производственная» реализация, используя абстрактный интерфейс, вы также можете создать «пробную» реализацию, которая работает в модульном тестировании.

  2. (Самая большая проблема). До дней с unique_ptr и переездом у вас был ограниченный выбор того, как хранить pImpl. Необработанный указатель, и у вас возникли проблемы с невозможностью копирования вашего класса. Старый auto_ptr не будет работать с заранее объявленным классом (во всяком случае, не со всеми компиляторами). Таким образом, люди начали использовать shared_ptr, который был хорош в том, чтобы сделать ваш класс копируемым, но, конечно, обе копии имели один и тот же базовый shared_ptr, чего вы не ожидали (измените один, и оба изменятся). Таким образом, решение часто заключалось в том, чтобы использовать необработанный указатель для внутреннего и сделать класс не подлежащим копированию и вернуть вместо него shared_ptr. Итак, два звонка на новый. (На самом деле 3 с учетом старого shared_ptr дали вам второй).

  3. Технически не совсем const-корректно, поскольку constness не передается до указателя на член.

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

Дойная корова
источник
3

Как говорили многие другие, идиома Pimpl позволяет достичь полной независимости от скрытия информации и компиляции, к сожалению, с затратами на потерю производительности (дополнительное перенаправление указателя) и дополнительной потребностью в памяти (сам указатель на элемент). Дополнительные затраты могут иметь решающее значение при разработке встроенного программного обеспечения, особенно в тех сценариях, когда память должна быть максимально экономной. Использование абстрактных классов C ++ в качестве интерфейсов приведет к тем же преимуществам при той же цене. На самом деле это показывает большой недостаток C ++, где, не возвращаясь к C-подобным интерфейсам (глобальные методы с непрозрачным указателем в качестве параметра), невозможно получить истинную независимость от скрытия информации и компиляции без дополнительных недостатков ресурса: это главным образом потому, что объявление класса, который должен быть включен его пользователями,

НККК
источник
3

Вот фактический сценарий, с которым я столкнулся, где эта идиома очень помогла. Недавно я решил поддержать DirectX 11, а также мою существующую поддержку DirectX 9 в игровом движке. Движок уже содержит большинство функций DX, поэтому ни один из интерфейсов DX не использовался напрямую; они были определены в заголовках как частные члены. Движок использует библиотеки DLL в качестве расширений, добавляя поддержку клавиатуры, мыши, джойстика и сценариев, как и многие другие расширения. Хотя большинство из этих DLL не использовали DX напрямую, им требовались знания и связь с DX просто потому, что они использовали заголовки, раскрывающие DX. При добавлении DX 11, эта сложность должна была резко возрасти, но без необходимости. Перемещение членов DX в Pimpl, определенный только в источнике, устранило это наложение. Вдобавок к этому сокращению библиотечных зависимостей,

Kit10
источник
2

Он используется на практике во многих проектах. Его полезность сильно зависит от типа проекта. Одним из наиболее известных проектов, использующих это, является Qt , где основная идея состоит в том, чтобы скрыть от пользователя код реализации или платформы (другие разработчики, использующие Qt).

Это благородная идея, но есть реальный недостаток: отладка. Пока код, скрытый в частных реализациях, имеет превосходное качество, это все хорошо, но если в нем есть ошибки, то у пользователя / разработчика есть проблема, потому что это просто тупой указатель на скрытую реализацию, даже если он имеет исходный код реализации.

Так как почти во всех дизайнерских решениях есть плюсы и минусы.

Хольгер Кретчмар
источник
9
это глупо, но это напечатано ... почему вы не можете следовать коду в отладчике?
UncleZeiv
2
Вообще говоря, для отладки в коде Qt вам нужно собрать Qt самостоятельно. Как только вы это сделаете, не возникнет проблем с входом в методы PIMPL и проверкой содержимого данных PIMPL.
Восстановите Монику
0

Одно преимущество, которое я вижу, состоит в том, что он позволяет программисту выполнять определенные операции довольно быстро:

X( X && move_semantics_are_cool ) : pImpl(NULL) {
    this->swap(move_semantics_are_cool);
}
X& swap( X& rhs ) {
    std::swap( pImpl, rhs.pImpl );
    return *this;
}
X& operator=( X && move_semantics_are_cool ) {
    return this->swap(move_semantics_are_cool);
}
X& operator=( const X& rhs ) {
    X temporary_copy(rhs);
    return this->swap(temporary_copy);
}

PS: Надеюсь, я не неправильно понял семантику ходов.

BenGoldberg
источник