Чи справді ідіома pImpl використовується на практиці?


165

Я читаю книгу Герба Саттера "Винятковий С ++", і в цій книзі я дізнався про ідіому pImpl. В основному, ідея полягає у створенні структури для privateоб'єктів а classта динамічно розподілити їх, щоб зменшити час компіляції (а також краще приховати приватні реалізації).

Наприклад:

class X
{
private:
  C c;
  D d;  
} ;

може бути змінено на:

class X
{
private:
  struct XImpl;
  XImpl* pImpl;       
};

та, у CPP, визначення:

struct X::XImpl
{
  C c;
  D d;
};

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

Чи варто використовувати його скрізь або з обережністю? І чи рекомендується цю техніку використовувати у вбудованих системах (де продуктивність дуже важлива)?


Це по суті те саме, що вирішити, що X є (абстрактним) інтерфейсом, а Ximpl - реалізацією? struct XImpl : public X. Мені це здається більш природним. Чи є якась інша проблема, яку я пропустив?
Аарон Мак-Дейд

@AaronMcDaid: Це схоже, але має ті переваги, що (a) функції-члени не повинні бути віртуальними, і (b) вам не потрібна фабрика або визначення класу реалізації, щоб миттєво це встановити.
Майк Сеймур

2
@AaronMcDaid Ідіома pimpl уникає викликів віртуальних функцій. Це також трохи більше C ++ - ish (для деяких концепцій C ++ - ish); ви викликаєте конструктори, а не фабричні функції. Я використовував і те, і інше, залежно від того, що є в існуючій кодовій базі --- ідіома pimpl (спочатку називалася ідіомою чеширської кішки, і передбачуючи опис Трави як мінімум на 5 років), здається, має більш тривалу історію і буде більше широко використовується в C ++, але в іншому випадку обидва працюють.
Джеймс Канзе

30
У C ++ pimpl слід реалізовувати, const unique_ptr<XImpl>а не XImpl*.
Ніл Г

1
"ніколи раніше не бачив подібного підходу, ні в компаніях, в яких я працював, ні в проектах з відкритим кодом". Qt навряд чи використовує його НЕ.
ManuelSchneid3r

Відповіді:


132

Отже, мені цікаво, що ця методика справді використовується на практиці? Чи варто використовувати його скрізь або з обережністю?

Звичайно, використовується. Я використовую це у своєму проекті, майже в кожному класі.


Причини використання ідіоми PIMPL:

Бінарна сумісність

Коли ви розробляєте бібліотеку, ви можете додавати / змінювати поля, XImplне порушуючи бінарної сумісності з вашим клієнтом (це може означати збої!). Оскільки двійковий макет Xкласу не змінюється, коли ви додаєте нові поля до Ximplкласу, безпечно додавати нову функціональність у бібліотеку в оновленнях незначних версій.

Звичайно, ви можете також додати нові публічні / приватні невіртуальні методи до X/ XImplбез порушення бінарної сумісності, але це нарівні зі стандартною технікою заголовка / реалізації.

Приховування даних

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

Час компіляції

Час компіляції скорочується, оскільки потрібно лише Xперебудовувати вихідний (імплементаційний) файл, коли ви додаєте / видаляєте поля та / або методи до XImplкласу (який відображає додавання приватних полів / методів у стандартній техніці). На практиці це звичайна операція.

За допомогою стандартної техніки заголовка / реалізації (без PIMPL), коли ви додаєте нове поле до Xкожного клієнта, який коли-небудь виділяє X(або в стеці, або в купі), потрібно перекомпілювати, оскільки він повинен регулювати розмір розподілу. Ну, і кожен клієнт, який ніколи не виділяє X, також повинен бути перекомпільований, але це просто накладні витрати (результат коду на стороні клієнта буде однаковим).

Більше того, стандартне розділення заголовка / реалізації XClient1.cppпотрібно перекомпілювати навіть тоді, коли приватний метод X::foo()був доданий Xта X.hзмінений, хоча, XClient1.cppможливо, не можна викликати цей метод з причини інкапсуляції! Як і вище, вона чиста накладні витрати і пов'язана з тим, як працюють системи побудови C ++ у реальному житті.

Звичайно, рекомпіляція не потрібна, коли ви просто змінюєте реалізацію методів (оскільки ви не торкаєтесь заголовка), але це нарівні зі стандартною технікою заголовка / реалізації.


Чи рекомендується цей прийом використовувати у вбудованих системах (де продуктивність дуже важлива)?

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


16
+1, оскільки він широко використовується в компанії, в якій я працюю, і з тих же причин.
Бенуа

9
також, бінарна сумісність
Амброз Біжак

9
У бібліотеці Qt цей метод використовується також у ситуаціях розумних покажчиків. Таким чином QString зберігає свій вміст як незмінний клас всередині. Коли публічний клас "копіюється", покажчик приватного члена копіюється замість всього приватного класу. Ці приватні класи потім також використовують розумні покажчики, тому ви, як правило, отримуєте сміття з більшістю класів, на додаток до значно покращеної продуктивності завдяки копіюванню вказівника замість повного копіювання класів
Тімоті Болдрідж

8
Навіть більше, ідіома pimpl Qt може підтримувати бінарну сумісність вперед та назад у межах однієї основної версії (у більшості випадків). IMO - це, безумовно, найважливіша причина його використання.
whitequark

1
Він також корисний для впровадження конкретного платформного коду, оскільки ви можете зберігати той самий API.
док

49

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

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

Переваги, які він може вам надати:

  • допомагає зберігати бінарну сумісність спільних бібліотек
  • приховуючи певні внутрішні деталі
  • зменшення циклів рекомпіляції

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

Можливі недоліки (також тут, залежно від впровадження та чи є вони для вас реальними недоліками):

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

Тож уважно дайте всім значення і оцініть самі. Для мене майже завжди виявляється, що використовувати ідіому pimpl не варто зусиль. Є лише один випадок, коли я особисто його використовую (або принаймні щось подібне):

Моя обгортка C ++ для statвиклику Linux . Тут структура із заголовка C може бути різною, залежно від того #defines, що встановлено. А оскільки мій заголовок обгортки не може контролювати їх усіх, я лише #include <sys/stat.h>в своєму .cxxфайлі та уникаю цих проблем.


2
Він майже завжди повинен використовуватися для системних інтерфейсів, щоб зробити систему інтерфейсу незалежною. Мій Fileклас (який відкриває значну частину інформації, statяка повертається під Unix) використовує той самий інтерфейс, як під Windows, так і з Unix, наприклад.
Джеймс Канзе

5
@JamesKanze: Навіть там я особисто спершу посиділа б на мить і подумала, чи не може бути достатньо декількох #ifdefс, щоб зробити обгортку якомога тоншою. Але у кожного різні цілі, важливо - це витратити час на роздуми над цим, а не сліпо слідувати чомусь.
ПлазмаHH

31

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

Причина полягає в тому, що екземпляр шаблону вимагає повної заяви, доступної там, де відбулася інстанція. (І це основна причина того, що ви не бачите методів шаблонів, визначених у файлах CPP)

Ви все ще можете посилатись на шаблонизовані підкласи, але оскільки ви повинні включити їх усі, всяка перевага «розв’язки реалізації» під час компіляції (уникаючи скрізь включати весь специфічний для платоформи код, скорочуючи компіляцію).

Це хороша парадигма для класичного ООП (на основі спадкування), але не для загального програмування (на основі спеціалізації).


4
Ви повинні бути більш точними: немає абсолютно ніяких проблем при використанні класів PIMPL в якості аргументів типу шаблону. Тільки якщо сам клас реалізації повинен бути параметризований на аргументах шаблону зовнішнього класу, він більше не може бути прихований від заголовка інтерфейсу, навіть якщо це все ще приватний клас. Якщо ви можете видалити аргумент шаблону, ви, звичайно, все ж можете зробити "належний" PIMPL. З видаленням типу ви також можете виконати PIMPL в базовому нешаблонному класі, а потім мати клас шаблону, що походить з нього.
Відновіть Моніку

22

Інші люди вже надали технічні покращення / недоліки, але я думаю, що варто зазначити наступне:

Перш за все, не будьте догматичними. Якщо pImpl працює у вашій ситуації, використовуйте його - не використовуйте його лише тому, що "це краще OO, оскільки він дійсно приховує реалізацію" тощо. Цитуючи C ++ FAQ:

інкапсуляція призначена для коду, а не людей ( джерело )

Просто для того, щоб навести приклад програмного забезпечення з відкритим кодом, де воно використовується і чому: OpenThreads, бібліотека різьблення, яку використовує OpenSceneGraph . Основна ідея полягає в тому, щоб видалити з заголовка (наприклад <Thread.h>) весь код платформи, оскільки внутрішні змінні стану (наприклад, ручки потоку) відрізняються від платформи до платформи. Таким чином можна скласти код проти вашої бібліотеки без будь-якого знання про ідіосинкразії інших платформ, оскільки все приховано.


12

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

Що стосується використання PIMPL для кожного класу, я б став обережним, оскільки всі ці переваги приносяться ціною: потрібен додатковий рівень непрямості для доступу до методів реалізації.


"необхідний додатковий рівень опосередкованості для доступу до методів реалізації." Це є?
xaxxon

@xaxxon так, так. pimpl повільніше, якщо методи низького рівня. ніколи не використовуйте його, наприклад, для речей, які живуть у тісній петлі.
Ерік Аронесті

@xaxxon Я б сказав, що загалом потрібен додатковий рівень. Якщо обшивка виконується, ніж ні. Але інлінінг не буде варіантом у коді, складеному в іншій dll.
Гіта

5

Я думаю, що це один із найважливіших інструментів для розв’язки.

Я використовував pimpl (та багато інших ідіом з виняткового C ++) у вбудованому проекті (SetTopBox).

Особливою метою цього idoim в нашому проекті було приховати типи використання XImpl класу. Зокрема, ми використовували його, щоб приховати деталі реалізацій для різних апаратних засобів, куди втягуватимуться різні заголовки. У нас були різні реалізації класів XImpl для однієї платформи та різні для іншої. Макет класу X залишався колишнім, незалежно від платформи.


4

Раніше я багато використовував цю техніку, але потім виявив себе віддаленим від неї.

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

Перевагами pImpl є:

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

  2. Якщо у вас набір класів (модуль) такий, що кілька класів мають доступ до одного і того ж "impl", але користувачі модуля будуть використовувати лише "відкриті" класи.

  3. Немає v-таблиці, якщо це вважається поганою справою.

Недоліки, які я знайшов у pImpl (де абстрактний інтерфейс працює краще)

  1. Хоча у вас може бути лише одна "виробнича" реалізація, використовуючи абстрактний інтерфейс, ви також можете створити "макет" впровадження, який працює в одиничному тестуванні.

  2. (Найбільше питання). До днів jedinstve_ptr та переміщення ви мали обмежений вибір щодо зберігання pImpl. Невідкладний покажчик і у вас виникли проблеми з тим, що ваш клас не можна скопіювати. Старий auto_ptr не працюватиме з наперед оголошеним класом (не у всіх компіляторах). Тож люди почали використовувати shared_ptr, що було приємно робити ваш клас копіюваним, але, звичайно, обидві копії мали однаковий базовий спільний_птр, якого ви можете не очікувати (змінити один і обидва змінені). Таким чином, рішенням було часто використовувати необроблений покажчик для внутрішнього та зробити клас не скопіюваним, а натомість повернути його спільний_ptr. Тож два дзвінки до нових. (Насправді 3 дані старого shared_ptr дали вам другий).

  3. Технічно не дуже const-коректно, оскільки constness не передається до вказівника-члена.

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


3

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


3

Ось власне сценарій, з яким я зіткнувся, де ця ідіома дуже допомогла. Нещодавно я вирішив підтримувати DirectX 11, а також мою існуючу підтримку DirectX 9 в ігровому двигуні. Двигун вже охопив більшість функцій DX, тому жоден з DX-інтерфейсів не використовувався безпосередньо; їх просто визначали в заголовках як приватних членів. Двигун використовує DLL як розширення, додаючи підтримку клавіатури, миші, джойстика та сценаріїв, як і тиждень, як і багато інших розширень. Хоча більшість цих DLL не використовували DX безпосередньо, вони вимагали знань та зв’язку з DX просто тому, що вони втягнули заголовки, які відкривали DX. Додавши DX 11, ця складність повинна була різко зрости, проте без потреби. Переміщення членів DX у Pimpl, визначений лише у джерелі, усунуло це накладення. Крім цього зменшення залежностей бібліотеки,


2

Він використовується на практиці у багатьох проектах. Його корисність значною мірою залежить від виду проекту. Одним з найбільш відомих проектів, що використовують це, є Qt , де основна ідея - приховати від користувача користувач (інші розробники, які використовують Qt).

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

Так як у майже всіх дизайнерських рішень є плюси і мінуси.


9
це німий, але він набраний ... чому ви не можете слідувати коду в налагоджувальній машині?
UncleZeiv

2
Взагалі кажучи, для налагодження коду Qt потрібно створити Qt самостійно. Як тільки ви це зробите, не виникає проблем перейти до методів PIMPL та перевірити вміст даних PIMPL.
Відновіть Моніку

0

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

X( X && move_semantics_are_cool ) : pImpl(NULL) {
    this->swap(move_semantics_are_cool);
}
X& swap( X& rhs ) {
    std::swap( pImpl, rhs.pImpl );
    return *this;
}
X& operator=( X && move_semantics_are_cool ) {
    return this->swap(move_semantics_are_cool);
}
X& operator=( const X& rhs ) {
    X temporary_copy(rhs);
    return this->swap(temporary_copy);
}

PS: Я сподіваюся, що я не розумію ходу семантики.

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