Действительно ли идиома pImpl используется на практике? - PullRequest
156 голосов
/ 23 января 2012

Я читаю книгу "Исключительный 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;
};

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

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

Ответы [ 11 ]

123 голосов
/ 23 января 2012

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

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


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

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

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

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

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

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

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

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

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

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

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


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

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

45 голосов
/ 23 января 2012

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

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

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

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

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

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

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

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

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

29 голосов
/ 23 января 2012

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

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

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

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

21 голосов
/ 23 января 2012

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

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

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

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

12 голосов
/ 23 января 2012

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

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

5 голосов
/ 23 января 2012

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

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

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

4 голосов
/ 10 августа 2015

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

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

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

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

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

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

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

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

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

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

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

3 голосов
/ 03 ноября 2016

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

3 голосов
/ 04 сентября 2015

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

2 голосов
/ 23 января 2012

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

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

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

...