zurück zum Artikel

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

Marc Mutz

Ü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.

Ü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.

Die sind betrĂ€chtlich. Durch das Wegkapseln sĂ€mtlicher Implementierungsdetails entsteht eine sowohl schlanke als auch langfristig stabile Schnittstelle (Header-Datei). Erstere fĂŒhrt zu lesbareren Klassendefinitionen, Letztere zur Wahrung der BinĂ€rkompatibilitĂ€t selbst bei umfangreichen Änderungen an der Implementierung.

So hat Nokias "Qt Development Frameworks"-Abteilung (vormals Trolltech) bei der Klassenbibliothek "Qt 4" bei mindestens zwei Gelegenheiten tiefgreifende Änderungen am Widget-Rendering durchgefĂŒhrt, ohne dass die Qt 4 benutzenden Programme hierfĂŒr auch nur neu gebunden hĂ€tten werden mĂŒssen.

Insbesondere in großen Projekten ist die Tendenz des Pimpl-Idoms nicht zu unterschĂ€tzen, ÜbersetzungslĂ€ufe dramatisch zu beschleunigen: Sowohl durch Einsparen von #include-Direktiven in Header-Dateien als auch durch die deutlich verringerte Änderungsfrequenz der Header-Dateien von
Pimpl-Klassen generell. Herb Sutter berichtet in "Exceptional C++" von regelmĂ€ĂŸiger Verdopplung der Geschwindigkeit, John Lakos gar von bis zu zwei GrĂ¶ĂŸenordnungen schnelleren ÜbersetzungslĂ€ufen.

Ein weiterer Vorzug des Designs besteht darin, dass Klassen mit d-Zeigern sich gut fĂŒr transaktionsorientierten beziehungsweise ausnahmesicheren Code eignen. Der Entwickler kann zum Beispiel auf das Copy-Swap-Idiom [3, Item 56] zurĂŒckgreifen, um dem Zuweisungsoperator Transaktionssemantik ("alles oder nichts") zu verleihen:

class Class {
// ...
void swap( Class & other ) {
std::swap( d, other.d );
}
Class & operator=( const Class & other ) {
// this may fail, but doesn't change *this
Class copy( other );
// this cannot fail, commits to *this:
swap( copy );
return *this;
}

Auch die Implementierung der neuen C++-0x-Verschiebeoperationen [5] ist trivial (und insbesondere fĂŒr alle Pimpl-Klassen gleich):

    // C++0x move semantics:
Class( Class && other ) : d( other.d ) { other.d = 0; }
Class & operator=( Class && other ) {
std::swap( d, other.d );
return *this;
}
// ...
}:

Sowohl Member-Swap als auch die Zuweisungsoperatoren lassen sich in diesem Modell inline definieren, ohne die Kapselung der Klasse zu kompromittieren; davon sollte der Entwickler regen Gebrauch machen.

Als letzter Vorzug sei die Option angesprochen, einen Teil der durch das Pimpl-Idiom verursachten zusĂ€tzlichen dynamischen Speicheranforderungen dadurch wieder einzusparen, dass die Private-Klasse ihre Datenfelder direkt einbettet, statt wie ĂŒblich nur einen Zeiger darauf zu deklarieren. Durch einmaliges "Pimplen" der ganzen Klasse entfĂ€llt sozusagen die Notwendigkeit, private Datenfelder komplexen Typs zur Entkopplung von Klassen voneinander nur durch Zeiger hindurch zu halten (was eine Art Pimpl pro Datenfeld darstellt).

Aus einer idiomatischen Qt-Dialogklasse

class QLineEdit;
class QLabel;
class MyDialog : public QDialog {
// ...
private:
// idiomatic Qt:
QLabel * m_loginLB;
QLineEdit * m_loginLE;
QLabel * m_passwdLB;
QLineEdit * m_passwdLE;
};

wird dann zum Beispiel

#include <QLabel>
#include <QLineEdit>

class MyDialog::Private {
// ...
// not idiomatic Qt, but less heap allocations:
QLabel loginLB;
QLineEdit loginLE;
QLabel passwdLB;
QLineEdit passwdLE;
};

Qt-Kenner mögen einwenden, der QDialog-Destruktor zerstöre die Kind-Widgets bereits; die direkte Aggregation löse demnach einen "double-delete" aus. Es ist in der Tat zutreffend, dass beim Einsatz der Technik die Gefahr besteht, Allokationssequenzfehler (double-delete, use-after-free etc.) zu begehen, insbesondere wenn sich Datenfelder und Klasse wechselseitig besitzen. Die gezeigte Transformation ist im vorliegenden Fall jedoch sicher, denn in Qt ist das Löschen von Kindern vor dem Löschen der Eltern immer erlaubt.

Die Methode ist besonders dann effektiv, wenn die auf die Weise aggregierten Datenfelder selbst Instanzen "gepimpelter" Klassen sind. Im gezeigten Beispiel ist das der Fall, und der Einsatz des Pimpl-Idioms spart vier dynamische Speicheranforderungen der GrĂ¶ĂŸe sizeof(void*) ein, erzeugt aber nur eine zusĂ€tzliche (grĂ¶ĂŸere). Das kann zu verbesserter Ausnutzung des dynamischen Speichers fĂŒhren, denn besonders kleine Speicheranforderungen erzeugen regelmĂ€ĂŸig besonders hohen Overhead.

Zudem "de-virtualisiert" der Kompiler in dem Szenario Aufrufe virtueller Funktionen viel eher; er entfernt also die durch die VirtualitĂ€t verursachte doppelte Indirektion beim Funktionsaufruf. Bei der Aggregation durch Zeiger ist dafĂŒr interprozedurale Optimierung notwendig. Ob das jedoch vor dem Hintergrund der Indirektion durch den d-Zeiger in der Summe einen Laufzeitgewinn oder -verlust darstellt, ist bei Bedarf im Einzelfall durch Messungen an konkreten Klassen nachzuweisen.

Sollte Profiling zeigen, dass die dynamische Speicheranforderung zu einem Flaschenhals wird, kann das "Fast Pimpl"-Idiom [2, Item 30] Abhilfe schaffen: In dieser Variante lÀsst sich ein schneller Allokator, etwa ein boost::singleton_pool [1] anstelle des globalen operator new() verwenden, um Private-Instanzen zu erzeugen.

Als bekanntes C++-Idiom erlaubt Pimpl es dem Klassenautor, die Schnittstelle von der Implementierung einer Klasse in einem Maße zu trennen, wie das in C++ eigentlich nicht vorgesehen ist. Als positiver Nebeneffekt beschleunigt der Einsatz von d-Zeigern ÜbersetzungslĂ€ufe, erleichtert die Implementierung von Transaktionssemantik und erlaubt durch erweiterte Mittel der Komposition mitunter zur Laufzeit effizientere Implementierungen.

Nicht alles ist jedoch gut beim Einsatz von d-Zeigern: Neben der zusÀtzlichen Private-Klasse und deren dynamischer Speicheranforderung bereiten die geÀnderte const-Methoden-Semantik und Allokationssequenzfehler Sorgen.

Mehr Infos

Ausblick

Der zweite und letzte Teil der Artikelserie wird nochmals einen genaueren Blick unter die Pimpl-Haube werfen, die "Roststellen" aufdecken und das Idiom mit einer ganzen Reihe von Accessoires "pimpen".

FĂŒr einiges davon zeigt der Autor im zweiten Teil Lösungen auf. Die KomplexitĂ€t nimmt dabei jedoch weiter zu, sodass in jedem konkreten Fall erneut zu prĂŒfen ist, ob die Vorteile des d-Zeigers seine Nachteile ĂŒberwiegen, im Zweifel pro in Frage kommender Klasse erneut. Pauschale Urteile sind wie so hĂ€ufig nicht möglich.

Marc Mutz
arbeitet als Software Engineer, Trainer und Consultant bei KDAB (Deutschland) GmbH & Co. KG.

  1. John Lakos; Large-Scale C++ Software Design; Addison-Wesley Longman, 1996
  2. Herb Sutter; Exceptional C++: 47 Engineering Puzzles, Programming Problems, and Solutions; Addison-Wesley Longman, 2000
  3. Herb Sutter, Andrei Alexandrescu: C++ Coding Standards: 101 Rules, Guidelines and Best Practices; Addison-Wesley Longman, 2004
  4. Daveed Vandervoorde: Modules in C++ (Revision 5), N2316=06-0176 (PDF [2])
  5. "Rvalue reference and move semantics" bei C++0x; auf engl. Wikipedia [3]
  6. Boost: Singleton Pool [4]

(ane [5])


URL dieses Artikels:
https://www.heise.de/-1097781

Links in diesem Artikel:
[1] http://www.boost.org/doc/libs/release/libs/pool/doc/interfaces/singleton_pool.html
[2] http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2316.pdf
[3] http://en.wikipedia.org/wiki/C++0x#Rvalue_reference_and_move_semantics
[4] http://www.boost.org/doc/libs/release/libs/pool/doc/interfaces/singleton_pool.html
[5] mailto:ane@heise.de