У чому сенс шаблону 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();

Тож у чому сенс PImpl?

Відповіді:


50

По-перше, PImpl зазвичай використовується для неполіморфних класів. І коли поліморфний клас має PImpl, він зазвичай залишається поліморфним, тобто все ще реалізує інтерфейси та переосмислює віртуальні методи з базового класу тощо. Так простіша реалізація PImpl - це не інтерфейс, а простий клас, що безпосередньо містить членів!

Існує три причини використовувати PImpl:

  1. Зробити бінарний інтерфейс (ABI) незалежним від приватних членів. Оновити спільну бібліотеку можна без повторної компіляції залежного коду, але лише до тих пір, поки двійковий інтерфейс залишиться тим самим. Тепер майже будь-яка зміна заголовка, за винятком додавання функції, яка не є членом та додавання функції невіртуального члена, змінює ABI. Ідіома PImpl переміщує визначення приватних членів у джерело і, таким чином, відокремлює ABI від їх визначення. Дивіться проблему крихкої бінарного інтерфейсу

  2. Коли заголовок змінюється, усі джерела, включаючи його, повинні бути перекомпільовані. А компіляція на C ++ досить повільна. Таким чином, переміщуючи визначення приватних членів у джерело, ідіома PImpl скорочує час компіляції, оскільки потрібно зменшити менше залежностей у заголовку, і ще більше скорочує час компіляції після модифікацій, оскільки залежні не потрібно перекомпілювати (нормально, це стосується і інтерфейсу + фабричної функції із прихованим класом бетону).

  3. Для багатьох класів C ++ виключення безпека є важливою властивістю. Часто вам потрібно скласти кілька класів в одному, щоб, якщо під час роботи над декількома членами кидає, жоден з членів не модифікований або у вас є операція, яка залишить члена в непослідовному стані, якщо він кидає, і вам потрібен об'єкт, що містить послідовний. У такому випадку ви реалізуєте операцію, створивши новий екземпляр PImpl та замінивши їх, коли операція пройде успішно.

Насправді інтерфейс також можна використовувати лише для приховування реалізації, але має такі недоліки:

  1. Додавання невіртуального методу не порушує ABI, але додавання віртуального. Отже, інтерфейси взагалі не дозволяють додавати методи, PImpl.

  2. Інтерфейс можна використовувати лише за допомогою вказівника / довідки, тому користувач повинен подбати про правильне управління ресурсами. З іншого боку, класи, що використовують 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файл, а отже, все інше, що включає його ...
Martin C. Martin

7

Я просто хочу звернутися до вашої точки ефективності. Якщо ви користуєтесь інтерфейсом, ви обов'язково створили віртуальні функції, які НЕ БУДУТЬ накреслені оптимізатором компілятора. Функції PIMPL можуть (і, ймовірно, будуть, тому що вони такі короткі) вбудовані (можливо, більше одного разу, якщо функція IMPL також мала). Виклик віртуальної функції неможливо оптимізувати, якщо ви не використовуєте повні оптимізації аналізу програми, які потребують дуже тривалого часу.

Якщо ваш клас PIMPL не використовується критично для продуктивності, то цей пункт не має великого значення, але ваше припущення, що продуктивність однакове, має місце лише в деяких ситуаціях, а не в усіх.


1
За допомогою оптимізації часу зв’язку більша частина накладних витрат на використання ідіоми pimpl видаляється. Як функції обгортки, так і функції реалізації можуть бути вбудовані, завдяки чому функція викликає ефективність, як звичайний клас, реалізований всередині заголовка.
годжі

Це справедливо лише в тому випадку, якщо ви використовуєте оптимізацію часу зв'язку. Суть PImpl в тому, що компілятор не має реалізації, навіть не функції переадресації. Хоча лінкер робить.
Мартін К. Мартін

5

Ця сторінка відповідає на ваше запитання. Багато людей погоджуються з вашим твердженням. Використання Pimpl має такі переваги перед приватними полями / членами.

  1. Зміна змінних приватних членів класу не вимагає перекомпіляції класів, які залежать від нього, таким чином, час стає швидшим, а FragileBinaryInterfaceProblem зменшується.
  2. У файлі заголовка не потрібно #include класи, які використовуються "за значенням" у змінних приватних членів, тому час компіляції швидше.

Ідіома Pimpl - це оптимізація часу компіляції, техніка розбиття залежності. Це скорочує великі файли заголовків. Це зводить ваш заголовок лише до загальнодоступного інтерфейсу. Довжина заголовка важлива в C ++, коли ви думаєте, як це працює. #include ефективно з'єднує заголовок з вихідним файлом - тому майте на увазі, що попередньо оброблені C ++ одиниці можуть бути дуже великими. Використання Pimpl може покращити час компіляції.

Існують і інші кращі методи усунення залежності - які передбачають вдосконалення вашого дизайну. Що представляють приватні методи? Яка єдина відповідальність? Чи повинні вони бути іншого класу?


PImpl взагалі не є оптимізацією щодо виконання. Виділення не є тривіальними, а pimpl означає додаткове виділення. Це техніка розбиття залежності та оптимізація часу компіляції .
Ян Худек

@JanHudec уточнив.
Дейв Гілєр

@DaveHillier, +1 для згадки FragileBinaryInterfaceProblem
ZijingWu
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.