Я читаю книгу «Исключительный 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;
};
Это кажется довольно интересным, но я никогда не видел такого подхода раньше, ни в компаниях, где я работал, ни в проектах с открытым исходным кодом, где я видел исходный код. Итак, мне интересно, эта техника действительно используется на практике?
Я должен использовать это везде или с осторожностью? И рекомендуется ли этот метод использовать во встроенных системах (где производительность очень важна)?
c++
oop
pimpl-idiom
Ренан Грейнерт
источник
источник
struct XImpl : public X
, Это кажется мне более естественным. Есть ли какая-то другая проблема, которую я пропустил?const unique_ptr<XImpl>
а неXImpl*
.Ответы:
Конечно это используется. Я использую это в своем проекте, почти в каждом классе.
Причины использования идиомы PIMPL:
Бинарная совместимость
Когда вы разрабатываете библиотеку, вы можете добавлять / изменять поля,
XImpl
не нарушая бинарную совместимость с вашим клиентом (что будет означать сбои!). Поскольку двоичный макетX
класса не изменяется при добавлении новых полей вXimpl
класс, безопасно добавлять новые функции в библиотеку при обновлении второстепенных версий.Конечно, вы также можете добавлять новые публичные / частные не виртуальные методы в
X
/XImpl
без нарушения бинарной совместимости, но это на уровне стандартной техники заголовка / реализации.Скрытие данных
Если вы разрабатываете библиотеку, особенно проприетарную, может быть желательно не раскрывать, какие другие библиотеки / методы реализации использовались для реализации открытого интерфейса вашей библиотеки. Либо из-за проблем с интеллектуальной собственностью, либо потому, что вы считаете, что у пользователей может возникнуть соблазн принять опасные предположения относительно реализации или просто нарушить инкапсуляцию, используя ужасные приемы приведения. PIMPL решает / смягчает это.
Время компиляции
Время компиляции уменьшается, поскольку
X
при добавлении / удалении полей и / или методов кXImpl
классу необходимо перестраивать только исходный файл (файл реализации), который сопоставляется с добавлением приватных полей / методов в стандартной технике). На практике это обычная операция.При использовании стандартного метода заголовка / реализации (без PIMPL) при добавлении нового поля
X
каждый клиент, который когда-либо выделяетX
(либо в стеке, либо в куче), должен быть перекомпилирован, поскольку он должен регулировать размер выделения. Хорошо, каждый клиент, который никогда не выделяет X, также должен быть перекомпилирован, но это просто накладные расходы (результирующий код на стороне клиента будет таким же).Более того, со стандартным разделением заголовок / реализация
XClient1.cpp
необходимо перекомпилировать, даже если частный методX::foo()
был добавленX
иX.h
изменен, даже если онXClient1.cpp
не может вызывать этот метод по причинам инкапсуляции! Как и выше, это просто накладные расходы и связано с тем, как работают реальные системы сборки C ++.Конечно, перекомпиляция не нужна, когда вы просто изменяете реализацию методов (потому что вы не трогаете заголовок), но это на уровне стандартной техники заголовка / реализации.
Это зависит от того, насколько сильна ваша цель. Тем не менее, единственный ответ на этот вопрос: измерить и оценить, что вы получаете и теряете. Кроме того, примите во внимание, что если вы не публикуете библиотеку, предназначенную для использования во встроенных системах вашими клиентами, применяется только преимущество времени компиляции!
источник
Кажется, что многие библиотеки используют его, чтобы оставаться стабильными в своих API, по крайней мере, для некоторых версий.
Но что касается всех вещей, вы никогда не должны использовать что-либо везде без осторожности. Всегда думайте, прежде чем использовать его. Оцените, какие преимущества это дает вам, и стоят ли они той цены, которую вы платите.
Преимущества, которые он может дать вам:
Это может или не может быть реальным преимуществом для вас. Как и для меня, меня не волнует несколько минут перекомпиляции. Конечные пользователи обычно тоже этого не делают, так как всегда компилируют его один раз и с самого начала.
Возможные недостатки (также здесь, в зависимости от реализации и являются ли они реальными недостатками для вас):
Так что тщательно оцените все и оцените это для себя. Для меня почти всегда получается, что использование идиомы pimpl не стоит усилий. Есть только один случай, когда я лично им пользуюсь (или хотя бы чем-то похожим):
Моя оболочка C ++ для
stat
вызова Linux . Здесь структура из заголовка C может отличаться в зависимости от того, что#defines
установлено. И так как мой заголовок оболочки не может контролировать их все, я только#include <sys/stat.h>
в своем.cxx
файле и избегаю этих проблем.источник
File
класс (который предоставляет большую часть информацииstat
, возвращаемой в Unix) использует один и тот же интерфейс, например, в Windows и Unix.#ifdef
секунд, чтобы сделать обертку максимально тонкой. Но у всех разные цели, главное - подумать об этом, а не слепо следовать чему-то.Согласитесь со всеми остальными в отношении товаров, но позвольте мне показать ограничение: не очень хорошо работает с шаблонами .
Причина в том, что создание экземпляра шаблона требует полного объявления, доступного там, где оно было выполнено. (И это главная причина, по которой вы не видите методы шаблона, определенные в файлах CPP)
Вы по-прежнему можете ссылаться на шаблонные подклассы, но, поскольку вы должны включить их все, все преимущества «разъединения реализации» при компиляции (избегая включения всего кода, специфичного для платформ, везде, сокращая компиляцию) теряются.
Является хорошей парадигмой для классического ООП (на основе наследования), но не для общего программирования (на основе специализации).
источник
Другие люди уже предоставили технические плюсы и минусы, но я думаю, стоит отметить следующее:
Прежде всего, не будьте догматичными. Если pImpl подходит для вашей ситуации, используйте его - не используйте его только потому, что «он лучше ОО, поскольку он действительно скрывает реализацию» и т. Д. Цитирование часто задаваемых вопросов по C ++:
Просто чтобы дать вам пример программного обеспечения с открытым исходным кодом, где оно используется и почему: OpenThreads, библиотека потоков, используемая OpenSceneGraph . Основная идея состоит в том, чтобы удалить из заголовка (например
<Thread.h>
) весь специфичный для платформы код, потому что внутренние переменные состояния (например, дескрипторы потоков) отличаются от платформы к платформе. Таким образом, вы можете скомпилировать код для вашей библиотеки без знания особенностей других платформ, потому что все скрыто.источник
Я бы в основном рассмотрел PIMPL для классов, которые могут быть использованы в качестве API другими модулями. Это имеет много преимуществ, поскольку перекомпиляция изменений, внесенных в реализацию PIMPL, не влияет на остальную часть проекта. Кроме того, для классов API они обеспечивают двоичную совместимость (изменения в реализации модуля не влияют на клиентов этих модулей, их не нужно перекомпилировать, поскольку новая реализация имеет тот же двоичный интерфейс - интерфейс, предоставляемый PIMPL).
Что касается использования PIMPL для каждого класса, я бы подумал об осторожности, потому что все эти преимущества обходятся дорого: для доступа к методам реализации необходим дополнительный уровень косвенности.
источник
Я думаю, что это один из самых фундаментальных инструментов для развязки.
Я использовал pimpl (и многие другие идиомы из Exceptional C ++) во встроенном проекте (SetTopBox).
Особая цель этого idoim в нашем проекте состояла в том, чтобы скрыть типы, используемые классом XImpl. В частности, мы использовали его, чтобы скрыть подробности реализаций для разных аппаратных средств, в которые будут вставляться разные заголовки. У нас были разные реализации классов XImpl для одной платформы и разные для другой. Планировка класса X осталась прежней независимо от платформы.
источник
Раньше я часто использовал эту технику, но потом отошел от нее.
Конечно, это хорошая идея, чтобы скрыть детали реализации от пользователей вашего класса. Однако вы также можете сделать это, заставив пользователей класса использовать абстрактный интерфейс, а детали реализации должны быть конкретным классом.
Преимущества pImpl:
Предполагая, что есть только одна реализация этого интерфейса, это более ясно, не используя абстрактный класс / конкретную реализацию
Если у вас есть набор классов (модуль), такой, что несколько классов обращаются к одному и тому же «impl», но пользователи модуля будут использовать только «открытые» классы.
Нет V-таблицы, если предполагается, что это плохо.
Недостатки, которые я нашел в pImpl (где абстрактный интерфейс работает лучше)
Хотя у вас может быть только одна «производственная» реализация, используя абстрактный интерфейс, вы также можете создать «пробную» реализацию, которая работает в модульном тестировании.
(Самая большая проблема). До дней с unique_ptr и переездом у вас был ограниченный выбор того, как хранить pImpl. Необработанный указатель, и у вас возникли проблемы с невозможностью копирования вашего класса. Старый auto_ptr не будет работать с заранее объявленным классом (во всяком случае, не со всеми компиляторами). Таким образом, люди начали использовать shared_ptr, который был хорош в том, чтобы сделать ваш класс копируемым, но, конечно, обе копии имели один и тот же базовый shared_ptr, чего вы не ожидали (измените один, и оба изменятся). Таким образом, решение часто заключалось в том, чтобы использовать необработанный указатель для внутреннего и сделать класс не подлежащим копированию и вернуть вместо него shared_ptr. Итак, два звонка на новый. (На самом деле 3 с учетом старого shared_ptr дали вам второй).
Технически не совсем const-корректно, поскольку constness не передается до указателя на член.
В общем, поэтому я перешел за годы от pImpl к использованию абстрактного интерфейса (и фабричных методов для создания экземпляров).
источник
Как говорили многие другие, идиома Pimpl позволяет достичь полной независимости от скрытия информации и компиляции, к сожалению, с затратами на потерю производительности (дополнительное перенаправление указателя) и дополнительной потребностью в памяти (сам указатель на элемент). Дополнительные затраты могут иметь решающее значение при разработке встроенного программного обеспечения, особенно в тех сценариях, когда память должна быть максимально экономной. Использование абстрактных классов C ++ в качестве интерфейсов приведет к тем же преимуществам при той же цене. На самом деле это показывает большой недостаток C ++, где, не возвращаясь к C-подобным интерфейсам (глобальные методы с непрозрачным указателем в качестве параметра), невозможно получить истинную независимость от скрытия информации и компиляции без дополнительных недостатков ресурса: это главным образом потому, что объявление класса, который должен быть включен его пользователями,
источник
Вот фактический сценарий, с которым я столкнулся, где эта идиома очень помогла. Недавно я решил поддержать DirectX 11, а также мою существующую поддержку DirectX 9 в игровом движке. Движок уже содержит большинство функций DX, поэтому ни один из интерфейсов DX не использовался напрямую; они были определены в заголовках как частные члены. Движок использует библиотеки DLL в качестве расширений, добавляя поддержку клавиатуры, мыши, джойстика и сценариев, как и многие другие расширения. Хотя большинство из этих DLL не использовали DX напрямую, им требовались знания и связь с DX просто потому, что они использовали заголовки, раскрывающие DX. При добавлении DX 11, эта сложность должна была резко возрасти, но без необходимости. Перемещение членов DX в Pimpl, определенный только в источнике, устранило это наложение. Вдобавок к этому сокращению библиотечных зависимостей,
источник
Он используется на практике во многих проектах. Его полезность сильно зависит от типа проекта. Одним из наиболее известных проектов, использующих это, является Qt , где основная идея состоит в том, чтобы скрыть от пользователя код реализации или платформы (другие разработчики, использующие Qt).
Это благородная идея, но есть реальный недостаток: отладка. Пока код, скрытый в частных реализациях, имеет превосходное качество, это все хорошо, но если в нем есть ошибки, то у пользователя / разработчика есть проблема, потому что это просто тупой указатель на скрытую реализацию, даже если он имеет исходный код реализации.
Так как почти во всех дизайнерских решениях есть плюсы и минусы.
источник
Одно преимущество, которое я вижу, состоит в том, что он позволяет программисту выполнять определенные операции довольно быстро:
PS: Надеюсь, я не неправильно понял семантику ходов.
источник