Czytam książkę „Wyjątkowy C ++” Herba Suttera iw tej książce poznałem idiom pImpl. Zasadniczo chodzi o stworzenie struktury dla private
obiektów a class
i 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)?
c++
oop
pimpl-idiom
Renan Greinert
źródło
źródło
struct XImpl : public X
. Wydaje mi się to bardziej naturalne. Czy jest jakiś inny problem, który przegapiłem?const unique_ptr<XImpl>
zamiastXImpl*
.Odpowiedzi:
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
XImpl
bez naruszania zgodności binarnej z klientem (co oznaczałoby awarie!). Ponieważ układ binarnyX
klasy nie zmienia się po dodaniu nowych pól doXimpl
klasy, 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
/XImpl
bez 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)
X
musi zostać przebudowany podczas dodawania / usuwania pól i / lub metod doXImpl
klasy (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.cpp
należy ponownie skompilować, nawet jeśli prywatna metodaX::foo()
została dodanaX
iX.h
zmieniona, nawet jeśliXClient1.cpp
nie 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.
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!
źródło
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:
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):
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
stat
wywołania linux . Tutaj struktura z nagłówka C może być inna, w zależności od tego, co#defines
jest ustawione. A ponieważ mój nagłówek opakowania nie może kontrolować ich wszystkich, mam tylko#include <sys/stat.h>
w swoim.cxx
pliku i unikam tych problemów.źródło
File
klasa (która ujawnia wiele informacjistat
zwracanych pod Uniksem) używa na przykład tego samego interfejsu zarówno pod Windows jak i Unix.#ifdef
aby 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ś.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).
źródło
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:
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.źródło
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.
źródło
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.
źródło
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:
Zakładając, że istnieje tylko jedna implementacja tego interfejsu, jest to jaśniejsze, jeśli nie używa się abstrakcyjnej klasy / konkretnej implementacji
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.
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)
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.
(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ą).
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).
źródło
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,
źródło
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,
źródło
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.
źródło
Jedną z korzyści, jakie widzę, jest to, że pozwala programiście na wykonanie pewnych operacji w dość szybki sposób:
PS: Mam nadzieję, że nie rozumiem semantyki ruchu.
źródło