Czy idiom pImpl jest rzeczywiście używany w praktyce?

165

Czytam książkę „Wyjątkowy C ++” Herba Suttera iw tej książce poznałem idiom pImpl. Zasadniczo chodzi o stworzenie struktury dla privateobiektów a classi dynamiczne przydzielanie ich, aby skrócić czas kompilacji (a także lepiej ukryć prywatne implementacje).

Na przykład:

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

można zmienić na:

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

aw CPP definicja:

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

Wydaje się to dość interesujące, ale nigdy wcześniej nie widziałem takiego podejścia, ani w firmach, w których pracowałem, ani w projektach open source, w których widziałem kod źródłowy. Więc zastanawiam się, czy ta technika jest naprawdę używana w praktyce?

Powinienem go używać wszędzie, czy ostrożnie? Czy ta technika jest zalecana do stosowania w systemach wbudowanych (gdzie wydajność jest bardzo ważna)?

Renan Greinert
źródło
Czy to zasadniczo to samo, co stwierdzenie, że X jest (abstrakcyjnym) interfejsem, a Ximpl jest implementacją? struct XImpl : public X. Wydaje mi się to bardziej naturalne. Czy jest jakiś inny problem, który przegapiłem?
Aaron McDaid
@AaronMcDaid: Jest podobna, ale ma zalety, że (a) funkcje składowe nie muszą być wirtualne i (b) nie potrzebujesz fabryki ani definicji klasy implementacji, aby ją utworzyć.
Mike Seymour
2
@AaronMcDaid Idiom pimpl pozwala uniknąć wywołań funkcji wirtualnych. To także trochę więcej C ++ - ish (dla pewnej koncepcji C ++ - ish); wywołujesz konstruktory zamiast funkcji fabrycznych. Użyłem obu, w zależności od tego, co jest w istniejącej bazie kodu - idiom pimpl (pierwotnie nazywany idiomem kota z Cheshire i wyprzedzający jego opis przez co najmniej 5 lat) wydaje się mieć dłuższą historię i być bardziej szeroko stosowany w C ++, ale poza tym oba działają.
James Kanze
30
W C ++ pimpl powinien być implementowany z const unique_ptr<XImpl>zamiast XImpl*.
Neil G
1
„Nigdy wcześniej nie widziałem takiego podejścia, ani w firmach, w których pracowałem, ani w projektach open source”. Qt prawie nigdy go NIE używa.
ManuelSchneid3r

Odpowiedzi:

132

Więc zastanawiam się, czy ta technika jest naprawdę używana w praktyce? Powinienem go używać wszędzie, czy ostrożnie?

Oczywiście, że jest używany. Używam go w swoim projekcie, na prawie wszystkich zajęciach.


Powody używania idiomu PIMPL:

Zgodność binarna

Kiedy tworzysz bibliotekę, możesz dodawać / modyfikować pola XImplbez naruszania zgodności binarnej z klientem (co oznaczałoby awarie!). Ponieważ układ binarny Xklasy nie zmienia się po dodaniu nowych pól do Ximplklasy, można bezpiecznie dodawać nowe funkcje do biblioteki w aktualizacjach wersji pomocniczych.

Oczywiście możesz również dodać nowe publiczne / prywatne niewirtualne metody do X/ XImplbez naruszania zgodności binarnej, ale jest to porównywalne ze standardową techniką nagłówka / implementacji.

Ukrywanie danych

Jeśli tworzysz bibliotekę, zwłaszcza tę zastrzeżoną, może być pożądane, aby nie ujawniać, jakie inne biblioteki / techniki implementacji zostały użyte do zaimplementowania publicznego interfejsu Twojej biblioteki. Albo z powodu problemów z własnością intelektualną, albo dlatego, że uważasz, że użytkownicy mogą ulec pokusie przyjęcia niebezpiecznych założeń dotyczących implementacji lub po prostu złamać hermetyzację, używając okropnych sztuczek castingowych. PIMPL rozwiązuje / łagodzi to.

Czas kompilacji

Czas kompilacji jest skrócony, ponieważ tylko plik źródłowy (implementacyjny) Xmusi zostać przebudowany podczas dodawania / usuwania pól i / lub metod do XImplklasy (co jest mapowane na dodawanie prywatnych pól / metod w standardowej technice). W praktyce to powszechna operacja.

Przy standardowej technice nagłówka / implementacji (bez PIMPL), kiedy dodajesz nowe pole do X, każdy klient, który kiedykolwiek przydzielił X(na stosie lub na stercie), musi zostać ponownie skompilowany, ponieważ musi dostosować rozmiar alokacji. Cóż, każdy klient, który nigdy nie przydzieli X, również musi zostać ponownie skompilowany, ale to tylko narzut (wynikowy kod po stronie klienta będzie taki sam).

Co więcej, przy standardowym rozdzieleniu nagłówka / implementacji XClient1.cppnależy ponownie skompilować, nawet jeśli prywatna metoda X::foo()została dodana Xi X.hzmieniona, nawet jeśli XClient1.cppnie można jej wywołać z powodów hermetyzacji! Podobnie jak powyżej, jest to czysty narzut i jest związany z tym, jak działają rzeczywiste systemy kompilacji C ++.

Oczywiście rekompilacja nie jest potrzebna, gdy po prostu modyfikujesz implementację metod (ponieważ nie dotykasz nagłówka), ale jest to porównywalne ze standardową techniką nagłówka / implementacji.


Czy ta technika jest zalecana do stosowania w systemach wbudowanych (gdzie wydajność jest bardzo ważna)?

To zależy od tego, jak potężny jest twój cel. Jednak jedyną odpowiedzią na to pytanie jest: zmierz i oceń, co zyskujesz, a co tracisz. Weź również pod uwagę, że jeśli nie publikujesz biblioteki przeznaczonej do użytku w systemach wbudowanych przez twoich klientów, obowiązuje tylko przewaga czasu kompilacji!

BЈовић
źródło
16
+1, ponieważ jest szeroko stosowany w firmie, w której pracuję, iz tych samych powodów.
Benoit,
9
także zgodność binarna
Ambroz Bizjak
9
W bibliotece Qt ta metoda jest również używana w sytuacjach inteligentnego wskaźnika. Więc QString zachowuje swoją zawartość jako niezmienną klasę wewnętrznie. Kiedy klasa publiczna jest „kopiowana”, zamiast całej klasy prywatnej kopiowany jest wskaźnik elementu członkowskiego prywatnego. Te prywatne lekcje wtedy również używać inteligentne kursory, więc można w zasadzie uzyskać zbieranie śmieci z większości klas, oprócz znacznie zwiększonej wydajności ze względu na wskaźnik kopiowania zamiast pełnej klasy kopiowania
Timothy Baldridge
8
Co więcej, dzięki idiomowi pimpl Qt może zachować zgodność binarną zarówno do przodu, jak i wstecz w ramach jednej głównej wersji (w większości przypadków). IMO to zdecydowanie najważniejszy powód, aby go używać.
whitequark
1
Jest również przydatny do implementacji kodu specyficznego dla platformy, ponieważ możesz zachować ten sam interfejs API.
doc
49

Wygląda na to, że wiele bibliotek używa go do stabilnego działania swojego API, przynajmniej dla niektórych wersji.

Ale jeśli chodzi o wszystkie rzeczy, nigdy nie powinieneś używać niczego wszędzie bez ostrożności. Zawsze pomyśl, zanim go użyjesz. Oceń, jakie daje Ci to korzyści i czy są warte ceny, jaką płacisz.

Korzyści, jakie może Ci to przynieść, to:

  • pomaga w zachowaniu zgodności binarnej bibliotek współdzielonych
  • ukrywanie niektórych szczegółów wewnętrznych
  • malejące cykle rekompilacji

Te mogą, ale nie muszą być dla Ciebie prawdziwymi korzyściami. Jak dla mnie, nie obchodzi mnie kilkuminutowa rekompilacja. Użytkownicy końcowi też tego nie robią, ponieważ zawsze kompilują je raz i od początku.

Możliwe wady to (również tutaj, w zależności od implementacji i czy są to dla Ciebie realne wady):

  • Wzrost zużycia pamięci z powodu większej liczby alokacji niż w przypadku wariantu naiwnego
  • zwiększony wysiłek konserwacyjny (musisz napisać przynajmniej funkcje spedycyjne)
  • utrata wydajności (kompilator może nie być w stanie wstawić rzeczy, jak to jest w przypadku naiwnej implementacji twojej klasy)

Dlatego uważnie nadaj wszystkiemu wartość i oceń to sam. Dla mnie prawie zawsze okazuje się, że posługiwanie się idiomem pimpl nie jest warte wysiłku. Jest tylko jeden przypadek, w którym osobiście go używam (lub przynajmniej coś podobnego):

Moje opakowanie C ++ dla statwywołania linux . Tutaj struktura z nagłówka C może być inna, w zależności od tego, co #definesjest ustawione. A ponieważ mój nagłówek opakowania nie może kontrolować ich wszystkich, mam tylko #include <sys/stat.h>w swoim .cxxpliku i unikam tych problemów.

PlasmaHH
źródło
2
Powinien być prawie zawsze używany w interfejsach systemu, aby system kodu interfejsu był niezależny. Moja Fileklasa (która ujawnia wiele informacji statzwracanych pod Uniksem) używa na przykład tego samego interfejsu zarówno pod Windows jak i Unix.
James Kanze
5
@JamesKanze: Nawet tam osobiście najpierw usiadłem na chwilę i zastanowiłem się, czy może nie wystarczy mieć kilka sekund, #ifdefaby opakowanie było tak cienkie, jak to tylko możliwe. Ale każdy ma inne cele, ważne jest, aby pomyśleć o tym zamiast ślepo podążać za czymś.
PlasmaHH
31

Zgadzam się z innymi co do towarów, ale pozwólcie, że przedstawię granicę: nie działa dobrze z szablonami .

Powodem jest to, że tworzenie instancji szablonu wymaga pełnej deklaracji dostępnej w miejscu, w którym miało miejsce. (I to jest główny powód, dla którego nie widzisz metod szablonowych zdefiniowanych w plikach CPP)

Nadal możesz odwoływać się do podklas opartych na szablonach, ale ponieważ musisz uwzględnić je wszystkie, każda korzyść z „oddzielenia implementacji” podczas kompilacji (unikanie umieszczania wszędzie kodu specyficznego dla platformy, skracanie kompilacji) zostaje utracona.

Jest dobrym paradygmatem dla klasycznego OOP (opartego na dziedziczeniu), ale nie dla programowania ogólnego (opartego na specjalizacji).

Emilio Garavaglia
źródło
4
Musisz być bardziej precyzyjny: nie ma absolutnie żadnego problemu, gdy używasz klas PIMPL jako argumentów typu szablonu. Tylko jeśli sama klasa implementacji musi być sparametryzowana na argumentach szablonu klasy zewnętrznej, nie może być już ukryta w nagłówku interfejsu, nawet jeśli nadal jest to klasa prywatna. Jeśli możesz usunąć argument szablonu, z pewnością nadal możesz zrobić „właściwy” PIMPL. Dzięki usunięciu typu można również wykonać PIMPL w podstawowej klasie niebędącej szablonem, a następnie wyprowadzić z niej klasę szablonu.
Przywróć Monikę
22

Inne osoby przedstawiły już techniczne zalety / wady, ale myślę, że warto zwrócić uwagę na następujące kwestie:

Przede wszystkim nie bądź dogmatyczny. Jeśli pImpl działa w twojej sytuacji, użyj go - nie używaj go tylko dlatego, że „jest lepszy OO, ponieważ naprawdę ukrywa implementację” itp. Cytując C ++ FAQ:

hermetyzacja dotyczy kodu, a nie ludzi ( źródło )

Podam przykład oprogramowania open source, w którym jest używane i dlaczego: OpenThreads, biblioteka wątków używana przez OpenSceneGraph . Głównym pomysłem jest usunięcie z nagłówka (np. <Thread.h>) Całego kodu specyficznego dla platformy, ponieważ wewnętrzne zmienne stanu (np. Uchwyty wątku) różnią się w zależności od platformy. W ten sposób można skompilować kod w swojej bibliotece bez znajomości cech charakterystycznych innych platform, ponieważ wszystko jest ukryte.

azalia
źródło
12

Rozważyłbym głównie PIMPL dla klas udostępnionych do wykorzystania jako API przez inne moduły. Ma to wiele zalet, gdyż sprawia, że ​​rekompilacja zmian wprowadzonych we wdrożeniu PIMPL nie wpływa na dalszą część projektu. Ponadto w przypadku klas API promują zgodność binarną (zmiany w implementacji modułu nie wpływają na klientów tych modułów, nie trzeba ich rekompilować, ponieważ nowa implementacja ma ten sam interfejs binarny - interfejs udostępniany przez PIMPL).

Jeśli chodzi o stosowanie PIMPL dla każdej klasy, rozważałbym ostrożność, ponieważ wszystkie te korzyści wiążą się z kosztami: wymagany jest dodatkowy poziom pośrednictwa, aby uzyskać dostęp do metod implementacji.

Ghita
źródło
„Wymagany jest dodatkowy poziom pośrednictwa w celu uzyskania dostępu do metod implementacji”. To jest?
xaxxon
@xaxxon tak, to jest. pimpl jest wolniejszy, jeśli metody są na niskim poziomie. na przykład nigdy nie używaj go do rzeczy, które żyją w ciasnej pętli.
Erik Aronesty
@xaxxon Powiedziałbym w ogólnym przypadku, gdy wymagany jest dodatkowy poziom. Jeśli wykonywane jest wstawianie, nie. Ale wstawianie nie byłoby opcją w kodzie skompilowanym w innej bibliotece dll.
Ghita,
5

Myślę, że jest to jedno z najbardziej podstawowych narzędzi do oddzielenia.

Używałem pimpl (i wielu innych idiomów z Exceptional C ++) w projekcie osadzonym (SetTopBox).

Szczególnym celem tego idoima w naszym projekcie było ukrycie typów używanych przez klasę XImpl. W szczególności użyliśmy go do ukrycia szczegółów implementacji dla różnych urządzeń, w których byłyby pobierane różne nagłówki. Mieliśmy różne implementacje klas XImpl dla jednej platformy i różne dla drugiej. Układ klasy X pozostał taki sam niezależnie od platformy.

user377178
źródło
4

W przeszłości często używałem tej techniki, ale potem odszedłem od niej.

Oczywiście dobrze jest ukryć szczegóły implementacji przed użytkownikami swojej klasy. Jednak można to również zrobić, zachęcając użytkowników klasy do używania abstrakcyjnego interfejsu, a szczegółem implementacji jest klasa konkretna.

Zalety pImpl to:

  1. Zakładając, że istnieje tylko jedna implementacja tego interfejsu, jest to jaśniejsze, jeśli nie używa się abstrakcyjnej klasy / konkretnej implementacji

  2. Jeśli masz zestaw klas (moduł) taki, że kilka klas ma dostęp do tego samego „impl”, ale użytkownicy modułu będą używać tylko „ujawnionych” klas.

  3. Brak tabeli v-table, jeśli zakłada się, że jest to zła rzecz.

Wady, które znalazłem w pImpl (gdzie abstrakcyjny interfejs działa lepiej)

  1. Chociaż możesz mieć tylko jedną implementację "produkcyjną", używając abstrakcyjnego interfejsu możesz także stworzyć "próbną" implementację, która działa w testowaniu jednostkowym.

  2. (Największy problem). Przed czasami unique_ptr i przeprowadzki mieliście ograniczone możliwości przechowywania pliku pImpl. Nieprzetworzony wskaźnik i miałeś problemy z niemożliwością kopiowania twojej klasy. Stary auto_ptr nie działałby z wcześniej zadeklarowaną klasą (i tak nie na wszystkich kompilatorach). Więc ludzie zaczęli używać shared_ptr, co było miłe w tworzeniu twojej klasy możliwej do skopiowania, ale oczywiście obie kopie miały ten sam podstawowy shared_ptr, którego możesz się nie spodziewać (zmodyfikuj jedną i obie są zmodyfikowane). Dlatego często rozwiązaniem było użycie surowego wskaźnika dla wewnętrznego wskaźnika i uniemożliwienie kopiowania klasy, a zamiast tego zwrócenie do niego shared_ptr. Więc dwa wezwania do nowego. (Właściwie 3, biorąc pod uwagę stare shared_ptr, dały ci drugą).

  3. Technicznie nie jest tak naprawdę stała-poprawna, ponieważ stała nie jest propagowana do wskaźnika elementu członkowskiego.

Ogólnie rzecz biorąc, w ostatnich latach odszedłem od pImpl i zamiast tego przeszedłem na abstrakcyjne użycie interfejsu (i metody fabryczne do tworzenia instancji).

Dojną krową
źródło
3

Jak wielu innych powiedziało, idiom Pimpl pozwala osiągnąć całkowitą niezależność od ukrywania informacji i kompilacji, niestety kosztem utraty wydajności (dodatkowe wskazanie wskaźnika) i dodatkowej pamięci (sam wskaźnik elementu członkowskiego). Dodatkowy koszt może mieć krytyczne znaczenie przy opracowywaniu oprogramowania wbudowanego, w szczególności w scenariuszach, w których należy maksymalnie oszczędzać pamięć. Użycie klas abstrakcyjnych C ++ jako interfejsów przyniosłoby te same korzyści przy tych samych kosztach. To pokazuje w rzeczywistości duży niedobór C ++, gdzie bez powtarzających się interfejsów podobnych do C (metody globalne z nieprzezroczystym wskaźnikiem jako parametrem) nie jest możliwe posiadanie prawdziwego ukrywania informacji i niezależności od kompilacji bez dodatkowych wad zasobów: dzieje się tak głównie dlatego, że deklaracja klasy, którą muszą uwzględnić jej użytkownicy,

ncsc
źródło
3

Oto rzeczywisty scenariusz, z którym się spotkałem, w którym ten idiom bardzo pomógł. Niedawno zdecydowałem się obsługiwać DirectX 11, a także moją istniejącą obsługę DirectX 9 w silniku gry. Silnik zawierał już większość funkcji DX, więc żaden z interfejsów DX nie był używany bezpośrednio; zostali po prostu zdefiniowani w nagłówkach jako członkowie prywatni. Silnik wykorzystuje biblioteki DLL jako rozszerzenia, dodając obsługę klawiatury, myszy, joysticka i skryptów, podobnie jak wiele innych rozszerzeń. Podczas gdy większość tych bibliotek DLL nie korzystała bezpośrednio z DX-a, wymagały one wiedzy i połączenia z DX po prostu dlatego, że pobierały nagłówki, które ujawniały DX. Wraz z dodaniem DX 11 ta złożoność miała wzrosnąć dramatycznie, jednak niepotrzebnie. Przeniesienie członków DX do Pimpl zdefiniowanego tylko w źródle wyeliminowało to narzucenie. Oprócz tego zmniejszenia zależności bibliotek,

Zestaw 10
źródło
2

Jest stosowany w praktyce w wielu projektach. Jego użyteczność zależy w dużej mierze od rodzaju projektu. Jednym z bardziej znanych projektów wykorzystujących to jest Qt , gdzie podstawową ideą jest ukrycie kodu implementacyjnego lub specyficznego dla platformy przed użytkownikiem (inni programiści używający Qt).

To szlachetny pomysł, ale ma prawdziwą wadę: debugowanie Tak długo, jak kod ukryty w prywatnych implementacjach jest najwyższej jakości, wszystko jest w porządku, ale jeśli są tam błędy, to użytkownik / programista ma problem, ponieważ jest to tylko głupi wskaźnik do ukrytej implementacji, nawet jeśli ma kod źródłowy implementacji.

Podobnie jak w przypadku prawie wszystkich decyzji projektowych, istnieją zalety i wady.

Holger Kretzschmar
źródło
9
jest głupi, ale jest wpisany ... dlaczego nie możesz śledzić kodu w debugerze?
UncleZeiv
2
Ogólnie rzecz biorąc, aby debugować kod Qt, musisz samodzielnie zbudować Qt. Gdy to zrobisz, nie ma problemu z przejściem do metod PIMPL i sprawdzeniem zawartości danych PIMPL.
Przywróć Monikę
0

Jedną z korzyści, jakie widzę, jest to, że pozwala programiście na wykonanie pewnych operacji w dość szybki sposób:

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: Mam nadzieję, że nie rozumiem semantyki ruchu.

BenGoldberg
źródło