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

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

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

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

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

  • Ясность кода
  • Ремонтопригодность кода
  • Спектакль

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

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

8 ответов

Решение

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

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

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

Ясность кода

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

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

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

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

Спектакль

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

Решение

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Другие вопросы по тегам