Wzorzec projektowy PImpl (Pointer to Implementation), znany również jako Opaque Pointer, powstał jako środek do oddzielenia interfejsu od implementacji klasy w C++. Jest to szczególnie ważne dla zapewnienia kompatybilności interfejsów binarnych (ABI), przyspieszenia kompilacji oraz ukrycia szczegółów implementacji przed użytkownikiem klasy.
Historia zagadnienia.
W wielu projektach C++ konieczna jest modyfikacja implementacji klas bez zmiany publicznego interfejsu i bez rekompilacji klientów tych klas. Problem polega na tym, że wszelkie zmiany w plikach nagłówkowych wymagają rekompilacji wszystkich zależnych modułów, co może być bardzo kosztowne w dużych bazach kodu. PImpl pozwala minimalizować rekompilację i zapewnia lepszą enkapsulację.
Problem.
Standardowy sposób definiowania klasy z prywatnymi członami w pliku nagłówkowym wymaga znajomości wszystkich tych członów podczas kompilacji. Przy rozszerzaniu lub zmianie ich konieczne jest przebudowanie wszystkich plików, które zawierają ten nagłówek. Dodatkowo, ujawnia to szczegóły implementacji/struktury klienta, co może negatywnie wpływać na bezpieczeństwo i integralność architektoniczną.
Rozwiązanie.
PImpl realizuje ukrywanie implementacji poprzez użycie wskaźnika na forward-deklarowaną strukturę-implementację (struct/class Impl), definiowaną w cpp. Pozwala to na zmianę implementacji bez wpływu na interfejs.
Przykład kodu:
// Widget.h class Widget { public: Widget(); ~Widget(); void doSomething(); private: struct Impl; Impl* pimpl; // opaque pointer }; // Widget.cpp #include "Widget.h" struct Widget::Impl { int secret; }; Widget::Widget() : pimpl(new Impl{42}) {} // secrecy inside Widget::~Widget() { delete pimpl; } void Widget::doSomething() { pimpl->secret += 1; }
Kluczowe cechy:
Czy można używać std::unique_ptr zamiast surowego wskaźnika w PImpl?
Tak, nowoczesne i bezpieczne podejście — użyć std::unique_ptr (lub std::shared_ptr, jeśli wymagana jest współdzielona własność). Umożliwia to prawidłowe zarządzanie pamięcią i unikanie pisania jawnego destruktora/operatora kopiowania dla surowego wskaźnika:
private: std::unique_ptr<Impl> pimpl;
Czy można uczynić klasę z PImpl przenośną, ale nie kopiowalną?
Tak, jeśli zapewnisz konstruktor/przypisanie przenoszące, ale usuniesz kopiujący. Na przykład:
Widget(Widget&&) noexcept = default; Widget& operator=(Widget&&) noexcept = default; Widget(const Widget&) = delete; Widget& operator=(const Widget&) = delete;
Czy występuje narzut na wydajność przy użyciu PImpl?
Tak, z powodu dereferencji wskaźnika i dodatkowego dynamicznego przydzielania pamięci (alokacja na stercie). Dla struktury krytycznych wydajności może to być istotną wadą.
Duża firma wdrożyła PImpl dla wszystkich klas, w tym dla prostych struktur danych, co doprowadziło do znacznego spowolnienia prostych operacji z powodu ciągłej dereferencji wskaźników.
Zalety:
Wady:
W projekcie z długowieczną biblioteką interfejsu użytkownika PImpl zastosowano tylko dla skomplikowanych widżetów z często zmieniającą się zawartością, zachowując stabilny ABI dla zewnętrznych klientów.
Zalety:
Wady: