Jaki jest sens wzorca PImpl, podczas gdy możemy używać interfejsu do tego samego celu w C ++?

49

Widzę dużo kodu źródłowego używającego idiomu PImpl w C ++. Zakładam, że jego celem jest ukrycie prywatnych danych / typu / implementacji, aby mógł on usunąć zależność, a następnie skrócić czas kompilacji i problem z nagłówkiem.

Ale klasy interfejsu / czysto abstrakcyjne w C ++ również mają tę możliwość, można ich również użyć do ukrycia danych / typu / implementacji. Aby wywołujący mógł widzieć interfejs podczas tworzenia obiektu, możemy zadeklarować metodę fabryczną w nagłówku interfejsu.

Porównanie jest następujące:

  1. Koszt :

    Koszt sposobu interfejsu jest niższy, ponieważ nie trzeba nawet powtarzać publicznej implementacji funkcji opakowania void Bar::doWork() { return m_impl->doWork(); }, wystarczy zdefiniować podpis w interfejsie.

  2. Dobrze zrozumiany :

    Technologia interfejsu jest lepiej rozumiana przez każdego programistę C ++.

  3. Wydajność :

    Wydajność interfejsu nie gorsza niż idiom PImpl, zarówno dodatkowy dostęp do pamięci. Zakładam, że wydajność jest taka sama.

Poniżej znajduje się pseudokod ilustrujący moje pytanie:

// 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;
};

Ten sam cel można zrealizować za pomocą interfejsu:

// 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();

Jaki jest sens PImpl?

ZijingWu
źródło

Odpowiedzi:

50

Po pierwsze, PImpl jest zwykle używany w klasach niepolimorficznych. A gdy klasa polimorficzna ma PImpl, zwykle pozostaje polimorficzna, co oznacza, że ​​nadal implementuje interfejsy i zastępuje metody wirtualne z klasy bazowej i tak dalej. Tak więc prostsza implementacja PImpl nie jest interfejsem, to prosta klasa bezpośrednio zawierająca elementy!

Istnieją trzy powody, aby używać PImpl:

  1. Dokonywanie binarny Interface (ABI), niezależnie od prywatnych osób. Możliwe jest aktualizowanie biblioteki współdzielonej bez ponownej kompilacji kodu zależnego, ale tylko tak długo, jak długo interfejs binarny pozostaje taki sam. Teraz prawie każda zmiana w nagłówku, z wyjątkiem dodawania funkcji nieosobistej i dodawania nieosobistej funkcji członka, zmienia ABI. Idiom PImpl przenosi definicję prywatnych członków do źródła i tym samym oddziela ABI od ich definicji. Zobacz: Fragile Binary Interface Problem

  2. Kiedy nagłówek się zmienia, wszystkie źródła łącznie z nim muszą zostać ponownie skompilowane. Kompilacja w C ++ jest raczej powolna. Tak więc, przenosząc definicje prywatnych elementów do źródła, idiom PImpl skraca czas kompilacji, ponieważ w nagłówku należy pobrać mniej zależności, i jeszcze bardziej skraca czas kompilacji po modyfikacjach, ponieważ zależności nie wymagają ponownej kompilacji (ok, dotyczy to również interfejsu + funkcja fabryczna z ukrytą klasą betonu).

  3. Dla wielu klas w C ++ bezpieczeństwo jest ważną właściwością. Często musisz skomponować kilka klas w jedną, aby jeśli podczas operacji na więcej niż jednym rzucie członka, żaden z członków nie został zmodyfikowany lub masz operację, która pozostawi członka w niespójnym stanie, jeśli wyrzuci i potrzebujesz obiektu zawierającego, aby pozostać zgodny. W takim przypadku implementujesz operację, tworząc nową instancję PImpl i zamieniając je, gdy operacja się powiedzie.

W rzeczywistości interfejs może być również używany tylko do ukrywania implementacji, ale ma następujące wady:

  1. Dodanie metody innej niż wirtualna nie psuje ABI, ale dodanie metody wirtualnej tak. Dlatego interfejsy w ogóle nie pozwalają na dodawanie metod, tak robi PImpl.

  2. Interfejs może być używany tylko za pomocą wskaźnika / odwołania, więc użytkownik musi zadbać o właściwe zarządzanie zasobami. Z drugiej strony klasy korzystające z PImpl są nadal typami wartości i wewnętrznie obsługują zasoby.

  3. Ukryta implementacja nie może być dziedziczona, klasa z PImpl może.

I oczywiście interfejs nie pomoże w wyjątkowym bezpieczeństwie. W tym celu potrzebujesz pośrednictwa w klasie.

Jan Hudec
źródło
Słuszna uwaga. Dodanie funkcji publicznej w Implklasie nie spowoduje przerwania ABI, ale dodanie funkcji publicznej w interfejsie ulegnie awarii ABI.
ZijingWu
1
Dodanie funkcji publicznej / metody nie musi przerywać ABI; zmiany ściśle addytywne nie są zmianami przerywającymi. Musisz jednak być bardzo ostrożny; kod pośredniczący między front-endem a back-endem musi radzić sobie z różnego rodzaju zabawą z wersjonowaniem. Wszystko staje się trudne. (Takie rzeczy powodują, że wiele projektów programistycznych woli C od C ++; pozwala im lepiej zrozumieć dokładnie, czym jest ABI.)
Donal Fellows
3
@DonalFellows: Dodanie funkcji publicznej / metody nie powoduje uszkodzenia ABI. Dodanie metody wirtualnej robi i robi to zawsze, chyba że użytkownik nie może odziedziczyć obiektu (co osiąga PImpl).
Jan Hudec
4
Wyjaśnienie, dlaczego dodanie metody wirtualnej psuje ABI. Ma to związek z powiązaniem. Łączenie z metodami innymi niż wirtualne odbywa się według nazwy, niezależnie od tego, czy biblioteka jest statyczna czy dynamiczna. Dodanie innej nie-wirtualnej metody jest po prostu dodaniem innej nazwy do łącza. Jednak metody wirtualne są indeksami w tabeli vtable, a kod zewnętrzny skutecznie łączy się według indeksu. Dodanie wirtualnej metody może przenosić inne metody w vtable bez żadnego kodu zewnętrznego.
SnakE
1
PImpl skraca także początkowy czas kompilacji. Na przykład, jeśli moja realizacja ma pole pewnego rodzaju szablonu (np unordered_set), bez PImpl, muszę #include <unordered_set>się MyClass.h. W przypadku PImpl muszę tylko dołączyć go do .cpppliku, a nie do .hpliku, a zatem wszystko, co go zawiera ...
Martin C. Martin
7

Chcę tylko odnieść się do twojego punktu wydajności. Jeśli korzystasz z interfejsu, musisz koniecznie stworzyć funkcje wirtualne, które NIE zostaną wprowadzone przez optymalizator kompilatora. Funkcje PIMPL można (i prawdopodobnie będą, ponieważ są tak krótkie) wstawiać (być może więcej niż jeden raz, jeśli funkcja IMPL jest również mała). Wirtualnego wywołania funkcji nie można zoptymalizować, chyba że użyjesz pełnej optymalizacji analizy programu, co zajmuje bardzo dużo czasu.

Jeśli Twoja klasa PIMPL nie jest używana w sposób krytyczny dla wydajności, ten punkt nie ma większego znaczenia, ale twoje założenie, że wydajność jest taka sama, obowiązuje tylko w niektórych sytuacjach, nie we wszystkich.

Chewy Gumball
źródło
1
Dzięki zastosowaniu optymalizacji czasu łącza, większość narzutów związanych z używaniem idiomu pimpl jest usuwana. Zarówno funkcje opakowania zewnętrznego, jak i funkcje implementacji mogą być wstawiane, dzięki czemu wywołania funkcji są efektywne jak normalna klasa zaimplementowana w nagłówku.
goji
Jest to prawdą tylko wtedy, gdy korzystasz z optymalizacji czasu łącza. Chodzi o to, że PImpl nie ma implementacji, nawet funkcji przekazywania. Linker to robi.
Martin C. Martin
5

Ta strona odpowiada na twoje pytanie. Wiele osób zgadza się z twoim twierdzeniem. Korzystanie z Pimpl ma następujące zalety w stosunku do prywatnych pól / członków.

  1. Zmiana prywatnych zmiennych członka klasy nie wymaga ponownej kompilacji zależnych od niej klas, dzięki czemu czasy są szybsze, a FragileBinaryInterfaceProblem jest zmniejszony.
  2. Plik nagłówkowy nie musi # zawierać klas, które są używane „według wartości” w zmiennych prywatnych członków, dlatego czasy kompilacji są szybsze.

Idiom Pimpl to optymalizacja czasu kompilacji, technika łamania zależności. Tnie duże pliki nagłówkowe. Zmniejsza to nagłówek tylko do interfejsu publicznego. Długość nagłówka jest ważna w C ++, gdy myślisz o tym, jak to działa. #include skutecznie łączy plik nagłówkowy z plikiem źródłowym - należy więc pamiętać, że wstępnie przetworzone jednostki C ++ mogą być bardzo duże. Korzystanie z Pimpl może poprawić czasy kompilacji.

Istnieją inne lepsze techniki przełamywania zależności - które wymagają ulepszenia projektu. Co oznaczają metody prywatne? Jaka jest jedyna odpowiedzialność? Czy powinni być inną klasą?

Dave Hillier
źródło
PImpl wcale nie jest optymalizacją pod względem czasu działania. Alokacje nie są trywialne, a pimpl oznacza dodatkową alokację. Jest to technika przełamująca zależności i optymalizująca tylko czas kompilacji .
Jan Hudec
@JanHudec wyjaśnione.
Dave Hillier
@DaveHillier, +1 za wzmiankę FragileBinaryInterfaceProblem
ZijingWu