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

Я читаю книгу "Исключительный С++" Херба Саттера, и в этой книге я узнал о идиоме 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;
};

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

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

Ответ 1

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

Конечно, он используется, и в моем проекте почти в каждом классе по нескольким причинам вы упомянули:

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

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

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

Ответ 2

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

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

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

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

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

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

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

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

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

Ответ 3

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

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

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

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

Ответ 4

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

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

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

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

Ответ 5

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

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

Ответ 6

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

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

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

Ответ 7

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

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

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

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

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

  • Нет v-таблицы, если это считается плохой.

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

  • В то время как у вас может быть только одна "производственная" реализация, используя абстрактный интерфейс, вы также можете создать "макет" inmplementation, которая работает в модульном тестировании.

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

  • Технически не очень const-correct, поскольку константа не распространяется на указатель-член.

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

Ответ 8

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

Ответ 9

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

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

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

Ответ 10

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

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: Надеюсь, я не ошибаюсь в семантике движения.

Ответ 11

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