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

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

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

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

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

  • ясность кода
  • ремонтопригодность код
  • производительность

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

EDIT: любые другие варианты для достижения того же самого будут приветствоваться предложениями.

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.

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

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

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

решение

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

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


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

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

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

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

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


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

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

это хорошо


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

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


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

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

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


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

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

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

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

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

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

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

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

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

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