Идиома Пимпл на практике - PullRequest
46 голосов
/ 09 мая 2009

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

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

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

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

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

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

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

Ответы [ 8 ]

35 голосов
/ 09 мая 2009

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

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

22 голосов
/ 09 мая 2009

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

11 голосов
/ 09 мая 2009

Ясность кода

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

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

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

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

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

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

Решение

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

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

7 голосов
/ 09 мая 2009

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

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

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

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

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

4 голосов
/ 09 мая 2009

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

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

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

3 голосов
/ 09 мая 2009

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

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

2 голосов
/ 23 февраля 2011

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

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

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

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

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

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

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

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

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

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

2 голосов
/ 09 мая 2009

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

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

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

...