Какой смысл в шаблоне PImpl, хотя мы можем использовать интерфейс для той же цели в C ++?

49

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

Но интерфейсные / чисто абстрактные классы в C ++ также имеют эту возможность, их также можно использовать для сокрытия данных / типа / реализации. И чтобы позволить вызывающей стороне просто видеть интерфейс при создании объекта, мы можем объявить фабричный метод в заголовке интерфейса.

Сравнение:

  1. Стоимость :

    Стоимость пути интерфейса ниже, потому что вам даже не нужно повторять реализацию функции общедоступной оболочки void Bar::doWork() { return m_impl->doWork(); }, вам просто нужно определить сигнатуру в интерфейсе.

  2. Хорошо поняли :

    Технология интерфейса лучше понимается каждым разработчиком C ++.

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

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

Ниже приведен псевдокод для иллюстрации моего вопроса:

// Forward declaration can help you avoid include BarImpl header, and those included in BarImpl header.
class BarImpl;
class Bar
{
public:
    // public functions
    void doWork();
private:
    // You don't need to compile Bar.cpp after changing the implementation in BarImpl.cpp
    BarImpl* m_impl;
};

Эту же цель можно реализовать с помощью интерфейса:

// Bar.h
class IBar
{
public:
    virtual ~IBar(){}
    // public functions
    virtual void doWork() = 0;
};

// to only expose the interface instead of class name to caller
IBar* createObject();

Так какой смысл Пимпл?

ZijingWu
источник

Ответы:

50

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

Есть три причины использовать PImpl:

  1. Создание бинарного интерфейса (ABI) независимым от приватных членов. Можно обновить общую библиотеку без перекомпиляции зависимого кода, но только до тех пор, пока двоичный интерфейс остается прежним. Теперь почти любое изменение в заголовке, за исключением добавления функции, не являющейся членом, и добавления функции, не являющейся виртуальной, изменяет ABI. Идиома PImpl перемещает определение частных членов в источник и, таким образом, отделяет ABI от их определения. См. Хрупкая проблема двоичного интерфейса

  2. При изменении заголовка все источники, включая его, должны быть перекомпилированы. И компиляция C ++ довольно медленная. Таким образом, перемещая определения закрытых членов в исходный код, идиома PImpl сокращает время компиляции, так как в заголовке нужно извлечь меньше зависимостей, и еще больше сокращает время компиляции после изменений, поскольку не нужно перекомпилировать зависимости (хорошо, это относится и к функции interface + factory со скрытым конкретным классом).

  3. Для многих классов в C ++ безопасность исключений является важным свойством. Часто вам нужно объединить несколько классов в один, чтобы, если во время операции над более чем одним броском члена, ни один из членов не был изменен, или у вас была операция, которая оставит член в несовместимом состоянии, если он выбрасывает, и вам нужно, чтобы содержащий объект оставался последовательны. В этом случае вы реализуете операцию, создавая новый экземпляр PImpl и меняя их местами после успешного завершения операции.

Фактически интерфейс также можно использовать только для сокрытия реализации, но имеет следующие недостатки:

  1. Добавление не виртуального метода не нарушает ABI, но добавление виртуального делает. Поэтому интерфейсы вообще не позволяют добавлять методы, как это делает PImpl.

  2. Inteface может использоваться только через указатель / ссылку, поэтому пользователь должен позаботиться о правильном управлении ресурсами. С другой стороны, классы, использующие PImpl, все еще являются типами значений и обрабатывают ресурсы внутренне.

  3. Скрытая реализация не может быть унаследована, класс с PImpl может.

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

Ян Худек
источник
Хорошая точка зрения. Добавление публичной функции в Implклассе не нарушит ABI, но добавление публичной функции в интерфейсе сломает ABI.
ZijingWu
1
Добавление публичной функции / метода не должно нарушать ABI; строго аддитивные изменения не нарушают изменений. Тем не менее, вы должны быть очень осторожны; код, являющийся посредником между внешним интерфейсом и внутренним интерфейсом, имеет дело со всеми видами удовольствия от управления версиями. Все становится сложно. (Именно из-за этого многие программные проекты предпочитают C-C ++; это позволяет им лучше понять, что такое ABI.)
Donal Fellows
3
@DonalFellows: Добавление публичной функции / метода не нарушает ABI. Добавление виртуального метода делает и делает это всегда, если только пользователю не запрещено наследовать объект (чего достигает PImpl).
Ян Худек
4
Чтобы выяснить, почему добавление виртуального метода нарушает ABI. Это имеет отношение к связи. Связывание с не виртуальными методами осуществляется по имени, независимо от того, является ли библиотека статической или динамической. Добавление другого не виртуального метода - это просто добавление другого имени для ссылки. Однако виртуальные методы являются индексами в vtable, а внешний код эффективно связывается по индексу. Добавление виртуального метода может перемещать другие методы в vtable без какого-либо способа извлечения внешнего кода.
SnakE
1
PImpl также уменьшает начальное время компиляции. Например, если моя реализация имеет поле какого-либо типа шаблона (например unordered_set), без PImpl, мне нужно #include <unordered_set>в MyClass.h. С PImpl мне нужно только включить его в .cppфайл, а не в .hфайл, и, следовательно, все остальное, что включает его ...
Мартин С. Мартин,
7

Я просто хочу обратиться к вашему выступлению. Если вы используете интерфейс, вы обязательно создали виртуальные функции, которые НЕ будут встроены оптимизатором компилятора. Функции PIMPL могут (и, вероятно, будут, потому что они такие короткие) встроены (возможно, более одного раза, если функция IMPL также мала). Вызов виртуальной функции нельзя оптимизировать, если вы не используете полную оптимизацию анализа программы, которая занимает очень много времени.

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

Chewy Gumball
источник
1
При использовании оптимизации времени соединения большая часть накладных расходов на использование языка pimpl устраняется. Как внешние функции-обертки, так и функции реализации могут быть встроены, что делает вызовы функций эффективными, как обычный класс, реализованный внутри заголовка.
Годжи
Это верно только в том случае, если вы используете оптимизацию во время соединения. Смысл PImpl в том, что компилятор не имеет реализации, даже функции пересылки. Линкер делает, хотя.
Мартин С. Мартин
5

Эта страница отвечает на ваш вопрос. Многие люди согласны с вашим утверждением. Использование Pimpl имеет следующие преимущества по сравнению с частными полями / членами.

  1. Изменение закрытых переменных-членов класса не требует перекомпиляции классов, которые зависят от него, поэтому время выполнения сокращается , а FragileBinaryInterfaceProblem уменьшается.
  2. Заголовочный файл не должен включать #include классы, которые используются 'по значению' в закрытых переменных-членах, поэтому время компиляции быстрее.

Идиома Pimpl - это оптимизация во время компиляции, метод разрушения зависимостей. Вырубает большие заголовочные файлы. Это сводит ваш заголовок только к общедоступному интерфейсу. Длина заголовка важна в C ++, когда вы думаете о том, как это работает. #include эффективно объединяет заголовочный файл с исходным файлом, так что имейте в виду, что предварительно обработанные модули C ++ могут быть очень большими. Использование Pimpl может улучшить время компиляции.

Существуют и другие лучшие методы разрушения зависимостей, которые включают в себя улучшение вашего дизайна. Что представляют собой частные методы? Какова единственная ответственность? Должны ли они быть другого класса?

Дэйв Хиллиер
источник
PImpl не является оптимизацией во время выполнения вообще. Распределение не является тривиальным и pimpl означает дополнительное распределение. Это техника нарушения зависимости и оптимизации только времени компиляции .
Ян Худек
@JanHudec уточнил.
Дэйв Хиллиер,
@DaveHillier, +1 за упоминание FragileBinaryInterfaceProblem
ZijingWu