Подтвердить что ты не робот

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

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

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

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

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

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

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

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

4b9b3361

Ответ 1

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

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

Ни одна из этих причин не предлагает подход "все или ничего". В первом из них вы только сглаживаете то, что хотите скрыть, тогда как во втором случае, вероятно, этого достаточно для классов, которые вы ожидаете изменить. Кроме того, для третьей и четвертой причин есть только скрытие нетривиальных элементов, которые, в свою очередь, требуют дополнительных заголовков (например, сторонней библиотеки или даже 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;
}

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

Ответ 2

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

Ответ 3

Ясность кода

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

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

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

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

Производительность

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

Решение

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

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

Ответ 4

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

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

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

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

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

Ответ 6

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

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

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

Ответ 7

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

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

Обоснование pImpl вместо этого может быть:

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

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

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

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

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

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

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

Ответ 8

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

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