Het ontwerp patroon PImpl (Pointer to Implementation), ook bekend als Opaque Pointer, is ontstaan als een middel om de interface en implementatie van een klasse in C++ te scheiden. Dit is vooral belangrijk voor het waarborgen van compatibiliteit van binaire interfaces (ABI), het versnellen van compilatie en het verbergen van implementatiedetails voor de gebruiker van de klasse.
Historie van de kwestie.
In een groot aantal C++ projecten moet de implementatie van klassen worden aangepast zonder de publieke interface te wijzigen en zonder de clients van deze klassen opnieuw te compileren. Het probleem is dat elke wijziging in de headerbestanden vereist dat alle afhankelijke modules opnieuw worden opgebouwd, wat extreem kostbaar kan zijn in grote codebases. PImpl minimaliseert de noodzaak tot herbouwen en biedt een betere encapsulatie.
Probleem.
De standaard manier om een klasse met privéleden in een headerbestand te definiëren vereist kennis van al deze leden tijdens de compilatie. Bij uitbreiding of wijziging ervan moeten alle bestanden die deze header includeren opnieuw worden gebouwd. Bovendien onthult dit de implementatiedetails/structuur aan de client, wat mogelijk een negatieve invloed heeft op de veiligheid en architectonische integriteit.
Oplossing.
PImpl implementeert het verbergen van de implementatie door gebruik te maken van een pointer naar een forward-gedeclareerde implementatiestructuur (Impl struct/class), gedefinieerd in de cpp. Dit maakt het mogelijk om de implementatie te wijzigen zonder de interface aan te passen.
Voorbeeld code:
// Widget.h class Widget { public: Widget(); ~Widget(); void doSomething(); private: struct Impl; Impl* pimpl; // ondoorzichtige pointer }; // Widget.cpp #include "Widget.h" struct Widget::Impl { int secret; }; Widget::Widget() : pimpl(new Impl{42}) {} // geheimhouding binnenin Widget::~Widget() { delete pimpl; } void Widget::doSomething() { pimpl->secret += 1; }
Belangrijke kenmerken:
Kan std::unique_ptr worden gebruikt in plaats van een ruwe pointer in PImpl?
Ja, de moderne en veilige benadering is om std::unique_ptr (of std::shared_ptr, indien gedeeld eigendom vereist is) te gebruiken. Dit maakt correct geheugenbeheer mogelijk en voorkomt dat er expliciet een destructor/kopieeroperator voor een ruwe pointer hoeft te worden geschreven:
private: std::unique_ptr<Impl> pimpl;
Kan een klasse met PImpl verplaatsbaar maar niet kopieerbaar worden gemaakt?
Ja, als je een move-constructor/operator levert, maar de kopieeroperator verwijderd. Bijvoorbeeld:
Widget(Widget&&) noexcept = default; Widget& operator=(Widget&&) noexcept = default; Widget(const Widget&) = delete; Widget& operator=(const Widget&) = delete;
Komt er een prestatielast bij het gebruik van PImpl?
Ja, vanwege derefereren van de pointer en extra dynamische geheugentoewijzing (heap allocatie). Voor kritisch-presterende structuren kan dit een aanzienlijk nadeel zijn.
Een groot bedrijf heeft PImpl geïmplementeerd voor allemaal classes, inclusief eenvoudige datastructuren, wat leidde tot aanzienlijke vertragingen van eenvoudige operaties door constante derefereren van pointers.
Voordelen:
Nadelen:
In een project met een langdurige gebruikersinterfacebibliotheek werd PImpl alleen toegepast op complexe widgets met vaak veranderlijke internals, terwijl een stabiele ABI voor externe clients behouden bleef.
Voordelen:
Nadelen: