C++: Vor- und Nachteile des d-Zeiger-Idioms, Teil 1

Über das auch als d-Zeiger, Compiler-Firewall und Cheshire Cat bekannte Pimpl-Idiom wurde schon viel geschrieben. heise Developer beleuchtet über die klassische Technik hinaus beachtenswerte Winkel des praktischen Konstrukts.

In Pocket speichern vorlesen Druckansicht
Lesezeit: 10 Min.
Von
  • Marc Mutz
Inhaltsverzeichnis

Über das auch als d-Zeiger, Compiler-Firewall und Cheshire Cat bekannte Idiom mit dem lustigen Namen (pimple = engl. für "Pickel") wurde schon viel geschrieben [1, 2, 3]. heise Developer beleuchtet in zwei Artikeln über die klassische Technik hinaus beachtenswerte Winkel des praktischen Konstrukts.

Dieser Beitrag rekapituliert zunächst das klassische Pimpl-Idiom (pointer-to-implementation), stellt im Weiteren seine Vorteile heraus und entwickelt auf der Grundlage weitere Idiome. Der zweite Beitrag wird sich damit beschäftigen, die typischen Nachteile zu mindern, die bei einem Einsatz von Pimpl unweigerlich entstehen.

Jeder C++-Programmierer ist wahrscheinlich einmal über eine Klassendefinition wie die folgende gestolpert:

class Class {
// ...
private:
class Private; // forward declaration
Private * d; // hide impl details
};

Mit ihr hat der Programmierer die Datenfelder seiner Class in eine geschachtelte Klasse Class::Private verschoben. Instanzen von Class enthalten nur noch einen Zeiger d auf ihre Class::Private-Instanz.

Um zu verstehen, aus welchem Grund der Autor die Indirektion benutzt hat, muss man etwas weiter ausholen und sich ein wenig mit dem C++-Modulsystem beschäftigen. Denn anders als in vielen anderen Programmiersprachen besitzt C++ (als Sprache) als eine der zahlreichen C-Altlasten keine eingebaute Unterstützung für Module. (Ein solcher Vorschlag wurde für C++0x eingebracht, aber nicht in den Standard aufgenommen [4].) Stattdessen lagert man die Deklarationen (aber üblicherweise nicht die Definitionen) der Modulfunktionen in Header-Dateien aus und bindet sie per #include-Präprozessordirektive in andere Module ein. Das führt allerdings dazu, dass den Header-Dateien eine Doppelrolle zukommt: Auf der einen Seite dienen sie als Schnittstelle zum Modul und auf der anderen als Deklaration für mögliche interne Implementierungsdetails.

In Zeiten von C funktionierte das prima: Die Implementierungsdetails von Funktionen sind per Deklaration und Definition vollständig gekapselt, und structs konnte man entweder "forward-deklarieren" (dann sind sie privat) oder direkt in der Header-Datei definieren (dann sind sie öffentlich). Die Klasse Class von oben sähe in "objektorientiertem C" vielleicht folgendermaßen aus:

struct Class;                           // forward declaration
typedef Class * Class_t; // -> only private members
void Class_new( Class_t * cls ); // Class::Class()
void Class_release( Class_t cls ); // Class::~Class()
int Class_f( Class_t cls, double num ); // int Class::f( double )
//...

Das funktioniert in C++ leider nicht. Es müssen Methoden innerhalb der Klasse deklariert werden. Da Klassen ohne Methoden recht langweilig sind, erscheinen daher in C++ normalerweise die Klassendefinitionen in den Header-Dateien. Da sich Klassen nicht wie Namensräume mehrfach öffnen und wieder schließen lassen, muss also die Header-Datei sämtliche Deklarationen von (Datenfeldern wie) Methoden enthalten:

class Class {
public:
// ... public methods ... ok
private:
// ... private data & methods ... don't want these here
};

Das Problem ist offensichtlich: Die Modulschnittstelle (Header-Datei) enthält hier zwingend Details der Implementierung; das ist immer eine schlechte Idee. Daher bedient man sich eines eigentlich recht hässlichen Tricks und lagert kurzum alle Implementierungsdetails (sowohl Datenfelder als auch private Methoden) in eine gesonderte Klasse aus:

// --- class.h ---
class Class {
public:
Class();
~Class();

// ... public methods ...

void f( double n );

private:
class Private;
Private * d;
};
// -- class.cpp --
#include <class.h>

class Class::Private {
public:
// ... private data & methods ...
bool canAcceptN( double num ) const { return num != 0 ; }
double n;
};

Class::Class()
: d( new Private ) {}

Class::~Class() {
delete d;
}

void Class::f( double n ) {
if ( d->canAcceptN( n ) )
d->n = n;
}

Da Class::Private in der Header-Datei nur in der Deklaration einer Zeigervariablen, also lediglich "dem Namen nach" und nicht "der Größe nach" (siehe [1]), verwendet wird, reicht eine Forward-Deklaration wie im Falle von C aus. Sämtliche Methoden von Class greifen nun durch d hindurch auf die Datenfelder und privaten Methoden in Class::Private zu.

Damit erhält man die Vorteile eines vollständig kapselnden Modulsystems auch in C++. Wegen des Rückgriffs auf Indirektion bezahlt der Entwickler diesen Vorteil allerdings mit einer zusätzlichen dynamischen Speicheranforderung (new Class::Private), der Indirektion beim Zugriff auf Datenfelder und private Methoden und dem vollständigen Verzicht auf (jedenfalls öffentliche) inline-Methoden. Wie der zweite Teil zeigen wird, verändert sich die Semantik von const-Methoden ebenfalls.

Bevor der zweite Teil des Artikels darauf eingeht, wie man die oben genannten Nachteile behebt oder zumindest in ihrer Bedeutung abschwächt, beleuchtet der Rest dieses Beitrags zunächst die Vorteile des Idioms.