Pimpl idiom vs Pure віртуальний інтерфейс класу


118

Мені було цікаво, що змусить програміста вибрати або ідіому Pimpl, або чистий віртуальний клас та спадщину.

Я розумію, що ідіома pimpl поставляється з однією явною додатковою непрямістю для кожного публічного методу та над створенням об'єкта.

Чистий віртуальний клас, з іншого боку, поставляється з неявним непрямим впливом (vtable) для успадкованої реалізації, і я розумію, що жодного створення об'єктів не накладні.
EDIT : Але вам потрібна фабрика, якщо ви створюєте об'єкт зовні

Що робить чистий віртуальний клас менш бажаним, ніж ідіома pimpl?


3
Чудове запитання, просто хотілося задати те саме. Дивіться також boost.org/doc/libs/1_41_0/libs/smart_ptr/sp_techniques.html
Франк

Відповіді:


60

Пишучи клас C ++, доречно подумати, чи буде це

  1. Тип Значення

    Копія за вартістю, особистість ніколи не важлива. Це доречно, щоб він був ключем у std :: map. Наприклад, клас "рядок", або "дата", або "складне число". «Копіювати» екземпляри такого класу має сенс.

  2. Тип юридичної особи

    Ідентичність важлива. Завжди передається посиланням, ніколи "значенням". Часто взагалі не має сенсу "копіювати" екземпляри класу. Коли це має сенс, зазвичай більш підходящим є поліморфний метод «Клон». Приклади: клас Socket, клас бази даних, клас "політика", все, що було б "закриттям" на функціональній мові.

І pImpl, і чистий абстрактний базовий клас - це методи зменшення залежностей від часу компіляції.

Однак я коли-небудь використовую pImpl для реалізації типів значень (тип 1), і лише іноді, коли мені дуже хочеться мінімізувати залежності зв'язку та часу компіляції. Часто турбуватися не варто. Як ви справедливо зазначаєте, є більше синтаксичних накладних витрат, оскільки вам потрібно написати методи переадресації для всіх публічних методів. Для класів другого типу я завжди використовую чистий абстрактний базовий клас із відповідними фабричними методами.


6
Будь ласка, дивіться коментар Пола де Врієзе до цієї відповіді . Pimpl та Pure Virtual суттєво відрізняються, якщо ви перебуваєте в бібліотеці і хочете поміняти місцями .so / .dll без відновлення клієнта. Клієнти посилаються на fimtend pimpl за назвою, тому достатньо збереження підписів старого методу. OTOH у чистому абстрактному випадку вони ефективно посилаються на індекс vtable, тому переупорядкування методів або вставлення в середину порушить сумісність.
SnakE

1
Ви можете додавати (або повторно замовляти) методи лише в передній частині класу Pimpl, щоб підтримувати бінарну порівнянність. Логічно кажучи, ви все-таки змінили інтерфейс, і це здається трохи хитким. Відповідь тут - розумний баланс, який також може допомогти при тестуванні одиниць за допомогою "введення залежності"; але відповідь завжди залежить від вимог. Сторонні автори бібліотеки (на відміну від використання бібліотеки у вашій власній організації) можуть сильно віддавати перевагу Pimpl.
Spacen Jasset

31

Pointer to implementationзазвичай полягає у приховуванні деталей структурної реалізації. Interfacesйдеться про інстанціювання різних реалізацій. Вони справді служать двом різним цілям.


13
необов’язково, я бачив класи, які зберігають кілька прищів залежно від потрібної реалізації. Часто це кажуть, що win32 impl vs linux impl щось, що потрібно реалізувати по-різному на кожній платформі.
Дуг Т.

14
Але ви можете використовувати Інтерфейс, щоб
роз’єднати

6
Хоча ви можете реалізувати pimpl за допомогою інтерфейсу, часто немає підстав для декупажу деталей реалізації. Тож немає підстав переходити до поліморфних. Причина для Pimpl, щоб зберегти деталі реалізації від клієнта (в C ++ , щоб тримати їх з заголовка). Ви можете зробити це за допомогою абстрактної бази / інтерфейсу, але, як правило, це зайва зайва кількість.
Майкл Берр

10
Чому це надмірно? Я маю на увазі, чи це повільніше метод інтерфейсу, ніж pimpl? Можуть бути логічні причини, але з практичної точки зору я б сказав, що це простіше зробити це з абстрактним інтерфейсом
Аркаіц Хіменес

1
Я б сказав, що абстрактний базовий клас / інтерфейс - це "звичайний" спосіб робити речі і дозволяє простіше тестувати через глузування
паульма

28

Ідіома pimpl допомагає зменшити залежність та час побудови, особливо у великих програмах, і мінімізує оголовки деталей про реалізацію вашого класу до одного блоку компіляції. Користувачі вашого класу не повинні навіть усвідомлювати існування прищів (за винятком виразного покажчика, до якого вони не прихильні!).

Абстрактні класи (чисті віртуальні) - це те, про що ваші клієнти повинні знати: якщо ви намагаєтеся використовувати їх для зменшення з'єднань та кругових посилань, вам потрібно додати певний спосіб дозволяти їм створювати ваші об’єкти (наприклад, за допомогою заводських методів чи класів, залежність ін'єкції або інші механізми).


17

Я шукав відповідь на те саме питання. Прочитавши деякі статті та деякі практики, я вважаю за краще використовувати інтерфейси «Чистий віртуальний клас» .

  1. Вони більш прямі вперед (це суб’єктивна думка). Ідіома Pimpl змушує мене відчувати, що я пишу код "для компілятора", а не для "наступного розробника", який прочитає мій код.
  2. Деякі рамки тестування мають пряму підтримку Mocking чистих віртуальних класів
  3. Це правда, що вам потрібна фабрика, щоб вона була доступна ззовні. Але якщо ви хочете використовувати поліморфізм: це теж "про", а не "кон". ... а простий заводський метод насправді не так сильно шкодить

Єдиний недолік ( я намагаюся дослідити це ) - це те, що ідіома pimpl може бути швидшою

  1. коли проксі-дзвінки вкладені, при успадкуванні обов'язково потрібен додатковий доступ до об'єкта VTABLE під час виконання
  2. слід пам’яті пам’яті pimpl public-proxy-класу менше (ви можете легко оптимізувати для швидших свопів та інших подібних оптимізацій)

21
Також пам’ятайте, що за допомогою успадкування ви вводите залежність від макета vtable. Щоб підтримувати ABI, ви більше не можете змінювати віртуальні функції (додавання в кінці - це щось безпечно, якщо немає дочірніх класів, які додають власні віртуальні методи).
Пол де Вріезе

1
^ Цей коментар тут повинен бути клейким.
CodeAngry

10

Я ненавиджу прищі! Вони роблять клас потворним і не читабельним. Всі методи перенаправляються на прищ. Ви ніколи не бачите в заголовках, які функціональні можливості має клас, тому не можете його переробити (наприклад, просто змінити видимість методу). Клас відчуває себе «вагітним». Я думаю, що використовувати iterface краще і дійсно достатньо, щоб приховати реалізацію від клієнта. Ви можете дозволити одному класу реалізувати декілька інтерфейсів, щоб зробити їх тонкими. Слід віддати перевагу інтерфейсам! Примітка: Вам не потрібен заводський клас. Актуальним є те, що клієнти класу спілкуються з його прикладами через відповідний інтерфейс. Приховування приватних методів я вважаю дивною параною і не бачу причин для цього, оскільки ми інтерфейсуємо.


1
Бувають випадки, коли ви не можете використовувати чисті віртуальні інтерфейси. Наприклад, коли у вас є якийсь спадковий код і у вас є два модулі, які вам потрібно розділити, не торкаючись їх.
AlexTheo

Як зазначав @Paul de Vrieze нижче, ви втрачаєте сумісність ABI при зміні методів базового класу, оскільки ви маєте неявну залежність від vtable класу. Це залежить від випадку використання, чи це проблема.
Х.

"Приховування приватних методів я вважаю дивною параною" Чи це не дозволяє вам приховати залежності і, отже, мінімізувати час компіляції, якщо залежність змінюється?
pooya13

Я також не розумію, наскільки фабрики легше переосмислити, ніж pImpl. Ви не залишаєте "інтерфейс" в обох випадках і не змінюєте реалізацію? На Фабриці ви повинні змінити один .h та один .cpp-файл, а в pImpl ви повинні змінити один .h та два .cpp-файли, але це стосується цього, і вам взагалі не потрібно змінювати файл cpp інтерфейсу pImpl.
pooya13

8

Існує дуже реальна проблема спільних бібліотек, що ідіома pimpl акуратно обходить, що чисті віртуали не можуть: ви не можете безпечно змінювати / видаляти дані членів класу, не змушуючи користувачів класу перекомпілювати їх код. Це може бути прийнятно за деяких обставин, але, наприклад, для системних бібліотек.

Щоб детально пояснити проблему, врахуйте наступний код у вашій спільній бібліотеці / заголовку:

// header
struct A
{
public:
  A();
  // more public interface, some of which uses the int below
private:
  int a;
};

// library 
A::A()
  : a(0)
{}

Компілятор висилає код у спільній бібліотеці, який обчислює адресу цілого числа, яке потрібно ініціалізувати, для певного зміщення (можливо, нуль у цьому випадку, тому що це єдиний член) з вказівника на об'єкт A, яким він знає this .

З боку користувача коду, a new Aспочатку виділить sizeof(A)байти пам'яті, а потім передасть вказівник на цю пам'ять A::A()конструктору якthis .

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

За допомогою pimpl'ing ви можете безпечно додавати та видаляти члени даних до внутрішнього класу, оскільки розподіл пам'яті та виклик конструктора відбуваються у спільній бібліотеці:

// header
struct A
{
public:
  A();
  // more public interface, all of which delegates to the impl
private:
  void * impl;
};

// library 
A::A()
  : impl(new A_impl())
{}

Все, що вам потрібно зробити зараз, - це захистити ваш публічний інтерфейс без членів даних, окрім вказівника на об’єкт реалізації, і ви захищені від цього класу помилок.

Редагувати: Я, можливо, варто додати, що єдиною причиною, про яку я тут говорю про конструктор, є те, що я не хотів надавати більше коду - така ж аргументація стосується всіх функцій, які мають доступ до членів даних.


4
Замість нікчемного *, я вважаю, що більш традиційним є передання оголосити клас реалізації:class A_impl *impl_;
Френк Крюгер

9
Я не розумію, ви не повинні оголошувати приватних членів у віртуальному чистому класі, який ви збираєтесь використовувати як інтерфейс. Ідея полягає в тому, щоб клас було чітко абстрактним, без розміру, лише чисті віртуальні методи, я нічого не бачу ви не можете робити через спільні бібліотеки
Аркаїц Хіменес

@Frank Krueger: Ти маєш рацію, я лінивий. @Arkaitz Jimenez: незначне непорозуміння; якщо у вас є клас, який містить лише чисті віртуальні функції, то говорити про спільні бібліотеки немає особливого сенсу. З іншого боку, якщо ви маєте справу з бібліотеками, що користуються спільним доступом, ваші загальнодоступні класи можуть бути розсудливими з причини, описаної вище.

10
Це просто неправильно. Обидва методи дозволяють вам приховати стан реалізації своїх класів, якщо ви зробите інший клас класом "чистої абстрактної бази".
Пол Холлінгсворт

10
Перше речення у вашому ансері передбачає, що чисті віртуалі з пов'язаним фабричним методом якимось чином не дозволяють приховати внутрішній стан класу. Це не правда. Обидві методи дозволяють приховати внутрішній стан класу. Різниця полягає в тому, як це виглядає користувачеві. pImpl дозволяє все ще представляти клас із семантикою значень, але також приховувати внутрішній стан. Чистий абстрактний базовий клас + заводський метод дозволяє представляти типи сутностей, а також дозволяє приховати внутрішній стан. Останнє саме так працює COM. Глава 1 "Основного COM" має велике невдоволення з цього приводу.
Пол Холлінгсворт

6

Ми не повинні забувати, що успадкування є сильнішою, тіснішою зв'язком, ніж делегування. Я також би врахував усі питання, що виникають у відповідях, що даються при вирішенні питання, які дизайнерські ідіоми використовувати для вирішення певної проблеми.


3

Хоча в інших відповідях широко висвітлено, можливо, я можу бути трохи більш чітким щодо однієї переваги pimpl над віртуальними базовими класами:

Підхід pimpl прозорий з точки зору користувача, тобто ви можете створити об'єкти класу на стеку та використовувати їх безпосередньо в контейнерах. Якщо ви спробуєте приховати реалізацію за допомогою абстрактного віртуального базового класу, вам потрібно буде повернути спільний вказівник до базового класу із заводу, ускладнюючи його використання. Розглянемо наступний еквівалентний код клієнта:

// Pimpl
Object pi_obj(10);
std::cout << pi_obj.SomeFun1();

std::vector<Object> objs;
objs.emplace_back(3);
objs.emplace_back(4);
objs.emplace_back(5);
for (auto& o : objs)
    std::cout << o.SomeFun1();

// Abstract Base Class
auto abc_obj = ObjectABC::CreateObject(20);
std::cout << abc_obj->SomeFun1();

std::vector<std::shared_ptr<ObjectABC>> objs2;
objs2.push_back(ObjectABC::CreateObject(13));
objs2.push_back(ObjectABC::CreateObject(14));
objs2.push_back(ObjectABC::CreateObject(15));
for (auto& o : objs2)
    std::cout << o->SomeFun1();

2

На мій погляд, ці дві речі служать зовсім іншим цілям. Мета ідіоми прищів - це, в основному, впоратися з вашою реалізацією, щоб ви могли робити такі речі, як швидкі заміни для свого роду.

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

Яблука та апельсини справді.


Я погоджуюся з яблуками / апельсинами. Але виявляється, що ви використовуєте pImpl для функціоналу. Моя мета - це переважно будівельно-технічне та інформаційне приховування.
xtofl

2

Найбільш прикрою проблемою щодо ідіоми pimpl є те, що вона надзвичайно важко підтримувати та аналізувати існуючий код. Таким чином, використовуючи pimpl, ви платите разом із часом та розчаруванням розробника лише на те, щоб "зменшити залежність та час збирання та мінімізувати показ заголовка деталей щодо реалізації". Вирішіть самі, якщо це дійсно того варте.

Особливо "час складання" - це проблема, яку ви можете вирішити за допомогою кращого обладнання або використовуючи такі інструменти, як Incredibuild (www.incredibuild.com, також вже включений у Visual Studio 2017), таким чином, це не впливає на ваш дизайн програмного забезпечення. Дизайн програмного забезпечення повинен бути, як правило, незалежним від способу побудови програмного забезпечення.


Ви також оплачуєте час розробника, коли час складання становить 20 хвилин замість 2, так що це трохи баланс, реальна модульна система допоможе тут багато.
Аркаіц Хіменез

IMHO, спосіб побудови програмного забезпечення взагалі не повинен впливати на внутрішній дизайн. Це зовсім інша проблема.
Трантор

2
Що ускладнює аналіз? Купка дзвінків у файлі реалізації, який переадресовується до класу Impl, не здається важким.
мабрахам

2
Уявіть реалізацію налагодження, де використовуються і pimpl, і інтерфейси. Починаючи з виклику в коді користувача A, ви простежуєтеся до інтерфейсу B, переходите до примхлого класу C, щоб нарешті почати налагодження класу реалізації D ... Чотири кроки, поки ви не зможете проаналізувати, що насправді відбувається. І якщо вся справа реалізована в DLL, ви, можливо, знайдете інтерфейс C десь між ....
Trantor

Навіщо використовувати інтерфейс з pImpl, коли pImpl також може виконати роботу інтерфейсу? (тобто це може допомогти вам досягти інверсії залежності)
pooya13
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.