Идиома Pimpl на практике

Было несколько вопросов о SO о идиоме pimpl , но мне больше любопытно, как часто она используется на практике.

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

С этим, это что-то, что должно быть принято на основе каждого classа или все или ничего? Является ли это оптимальным или личным предпочтением?

Я понимаю, что это несколько субъективно, поэтому позвольте мне перечислить мои главные приоритеты:

  • Четкость кода
  • Поддержка кода
  • Представление

Я всегда предполагаю, что в какой-то момент мне нужно будет выставить мой код как библиотеку, так что это тоже соображение.

РЕДАКТИРОВАТЬ: любые другие варианты для достижения того же, были бы приветствуемыми предложениями.

Я бы сказал, что независимо от того, выполняете ли вы это по classу или по принципу «все или ничего», зависит от того, почему вы ищите первую идиому pimpl. Мои причины при создании библиотеки были следующими:

  • Хотел скрывать реализацию, чтобы избежать раскрытия информации (да, это был не проект FOSS 🙂
  • Требуется скрыть реализацию, чтобы сделать клиентский код менее зависимым. Если вы создаете общую библиотеку (DLL), вы можете изменить свой class pimpl, даже не перекомпилируя приложение.
  • Требуется сократить время, затрачиваемое на компиляцию classов с использованием библиотеки.
  • Требуется исправить конфликт пространства имен (или аналогичный).

Ни одна из этих причин не требует подхода «все или ничего». В первом вы только уклоняетесь от того, что хотите скрыть, тогда как во втором случае, вероятно, этого достаточно для classов, которые вы ожидаете изменить. Кроме того, для третьей и четвертой причин есть только возможность скрывать нетривиальные элементы, которые, в свою очередь, требуют дополнительных заголовков (например, сторонней библиотеки или даже STL).

В любом случае, я хочу сказать, что я обычно не считаю что-то вроде этого слишком полезным:

class Point { public: Point(double x, double y); Point(const Point& src); ~Point(); Point& operator= (const Point& rhs); void setX(double x); void setY(double y); double getX() const; double getY() const; private: class PointImpl; PointImpl* pimpl; } 

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

Одним из самых больших применений pimpl ideom является создание стабильного C ++ ABI. Почти каждый class Qt использует указатель «D», который является своего рода pimpl. Это позволяет выполнять гораздо более легкие изменения без нарушения ABI.

Четкость кода

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

Ремонтопригодность

Для удобства обслуживания classа pimpl’d я лично нахожу дополнительное разыменование в каждом доступе элемента данных утомительным. Аксессуры не могут помочь, если данные являются чисто частными, потому что в любом случае вы не должны открывать для него аксессор или мутатор, и вы застряли с постоянно разыменовывая пимпл.

Для поддерживаемости производных classов я считаю, что идиома – это чистая победа во всех случаях, потому что в файле заголовка перечисляются меньше нерелевантных деталей. Время компиляции также улучшено для всех единиц компиляции клиента.

Представление

Потери производительности во многих случаях незначительны и незначительны. В долгосрочной перспективе он находится в порядке величины потери производительности виртуальных функций. Мы говорим о дополнительном разыменовании для каждого элемента данных, а также о распределении динамической памяти для pimpl, а также о выпуске памяти при уничтожении. Если class pimpl’d не имеет доступа к своим членам данных часто, объекты classа pimpl’d создаются часто и недолговечны, тогда динамическое распределение может выдержать лишние различия.

Решение

Я думаю, что classы, в которых производительность имеет решающее значение, так что одно дополнительное разыменование или распределение памяти делает существенную разницу, не следует использовать pimpl независимо от того, что. Базовый class, в котором это снижение производительности незначительно, и файл заголовка широко # include’d, вероятно, должен использовать pimpl, если время компиляции значительно улучшилось. Если время компиляции не уменьшается, это зависит от вашего кода.

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

pImpl очень полезен, когда вы приступаете к реализации std :: swap и operator = с надежной гарантией исключения. Я склонен сказать, что если ваш class поддерживает любой из них и имеет более одного нетривиального поля, то обычно это больше не относится к предпочтениям.

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

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

Вы всегда можете добавить pImpl позже и объявить, что с этого дня клиентам не придется перекомпилировать только потому, что вы добавили личное поле.

Таким образом, ничто из этого не предполагает подхода «все или ничего». Вы можете выборочно делать это для classов, где это дает вам пользу, а не для тех, которые у него нет, и передумать позже. Реализация, например, iteratorов, поскольку pImpl звучит как слишком много дизайна …

Эта идиома очень помогает во время компиляции больших проектов.

Внешняя ссылка

Это тоже хорошо

Обычно я использую его, когда хочу избежать заголовочного файла, загрязняющего мою кодовую базу. Windows.h – прекрасный пример. Это так плохо себя ведет, я лучше убью себя, чем везде. Поэтому, предполагая, что вам нужен API на основе classов, скрытие его за classом pimpl решительно решает проблему. (Если вы хотите просто разоблачить индивидуальную функцию, это можно просто объявить, конечно, не помещая в class pimpl)

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

Я использую идиому в нескольких местах в своих собственных библиотеках, в обоих случаях для того, чтобы полностью разделить интерфейс от реализации. У меня, например, class читателя XML, полностью объявленный в файле .h, который имеет PIMPL для classа RealXMLReader, который объявлен и определен в непубличных файлах .h и .cpp. RealXMlReader, в свою очередь, является удобной оболочкой для синтаксического анализатора XML, который я использую (в настоящее время Expat).

Эта компоновка позволяет мне перейти от Expat в будущем на другой синтаксический анализатор XML без необходимости перекомпилировать весь код клиента (мне все равно нужно повторно связать, конечно).

Обратите внимание, что я не делаю этого для соображений производительности во время компиляции, только для удобства. Есть несколько PnPL fabnatics, которые настаивают на том, что любой проект, содержащий более трех файлов, будет несовместимым, если вы не используете PIMPL на всем протяжении. Примечательно, что эти люди никогда не приводят никаких фактических доказательств, но лишь смутно ссылаются на «Latkos» и «экспоненциальное время».

pImpl будет работать лучше всего, когда у нас есть семантика r-значения.

«Альтернатива» pImpl, которая также позволит скрыть детали реализации, заключается в использовании абстрактного базового classа и реализации реализации в производном classе. Пользователи создают какой-то «фабричный» метод для создания экземпляра и обычно используют указатель (возможно, общий) для абстрактного classа.

Обоснованием pImpl может быть следующее:

  • Сохранение на v-столе. Да, но ваш компилятор включит всю переадресацию и действительно ли вы ничего сохраните.
  • Если ваш модуль содержит несколько classов, которые знают друг о друге подробно, но для внешнего мира вы скрываете это.

Семантикой classа контейнера для pImpl может быть: – Не скопируемый, не назначаемый … Итак, вы «новый» ваш pImpl при построении и «удаляете» при уничтожении – совместно. Таким образом, у вас есть shared_ptr, а не Impl *

С shared_ptr вы можете использовать декларацию forward, если class завершен в точке деструктора. Ваш деструктор должен быть определен, даже если по умолчанию (что, вероятно, будет).

  • замена. Вы можете реализовать «может быть пустым» и реализовать «swap». Пользователи могут создать экземпляр одного и передать ему неконстантную ссылку, чтобы он был заполнен, с «свопом».

  • 2-ступенчатая конструкция. Вы строите пустой, затем вызываете «load ()», чтобы заполнить его.

shared – единственный, у меня есть даже удаленная симпатия без семантики r-значения. С их помощью мы также можем реализовать несовместимые непривязанные должным образом. Мне нравится называть функцию, которая дает мне одну.

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

Давайте будем гением компьютера.