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

Seite 2: Vorteile

Inhaltsverzeichnis

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 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)
  5. "Rvalue reference and move semantics" bei C++0x; auf engl. Wikipedia
  6. Boost: Singleton Pool

(ane)