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

Sprachen  –  0 Kommentare

Über das 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]. Nachdem ein erster Artikel auf heise Developer zunächst das klassische Pimpl-Idiom vorgestellt und dessen Vorteile herausgestellt hat, geht es im zweiten Teil darum, typische Nachteile zu mindern, die beim Einsatz von Pimpl unweigerlich entstehen.

Ein erster leicht zu übersehender Nachteil des Idioms ist das Problem mit den flachen Konstanten. Beim Einsatz von Pimpl greifen alle Methoden lediglich durch d hindurch auf die Datenfelder der Klasse zu:

SomeThing & Class::someThing() const {
return d->someThing;
}

Es fällt erst auf den zweiten Blick auf, dass der Code durch die Indirektion ein Sicherheitsmerkmal von C++ umgeht: Da die Funktion als const-Methode deklariert ist, ist this innerhalb von someThing() vom Typ const Class*, mithin ist d vom Typ Class::Private * const. Das reicht aber nicht, um Schreibzugriffe auf Datenfelder von Class::Private zu unterbinden, denn nur d ist const, nicht aber *d.

Man erinnere sich: In C/C++ ist const nicht tief, sondern flach:

const int * pci;        // pointer to const int
int * const cpi; // const pointer to int
const int * const cpci; // const pointer to const int

*pci = 1; // error: *pci is const
*cpi = 1; // ok! *cpi isn't const
*cpci = 1; // error: *cpci is const

int i;
pci = &i; // ok
cpi = &i; // error: cpi is const
cpci = &i; // error: cpci is const

Bei Verwendung von Pimpl können also sowohl const als auch Nicht-const-Methoden schreibend auf die Datenfelder des Objekts zugreifen. Im der Version ohne Pimpl unterbindet das der Compiler noch wirksam.

Die Lücke im Typsystem ist sicherlich in den meisten Fällen nicht gewünscht und sollte geschlossen werden. Das geht mit deep_const_ptr oder einem Paar von d_func()-Methoden. Ersteres ist eine einfache intelligente Zeigerklasse, die tiefes const für ausgewählte Zeigervariablen "nachrüstet". Die auf das Wesentliche reduzierte Klassendefinition könnte wie folgt aussehen:

template <typename T
class deep_const_ptr {
T * p;
public:
explicit deep_const_ptr( T * t ) : p( t ) {}

const T & operator*() const { return *p; }
T & operator*() { return *p; }

const T * operator->() const { return p; }
T * operator->() { return p; }
};

Durch den Trick, operator*() und operator->() jeweils nach const und Nicht-const zu überladen, wird das const von d an *d "durchgereicht". Ein einfaches Ersetzen von Private * d; durch deep_const_ptr<Private> d; schließt nun die Lücke im Typsystem wirksam. Es muss aber nicht gleich eine intelligente Zeigerklasse her. Der Trick mit dem Überladen nach const beziehungsweise Nicht-const funktioniert auch direkt mit Methoden auf Class:

class Class {
// ...
private:
const Private * d_func() const { return _d; }
Private * d_func() { return _d; }
private:
Private * _d;
};

Statt in Methodenimplementierungen auf _d zuzugreifen, geht man immer durch d_func():

void Class::f() const {
const Private * d = f_func();
// use 'd' ...
}

Sicherlich hindert einen niemand daran, wieder _d direkt zu verwenden – etwas, das mit deep_const_ptr nicht möglich ist. Daher erfordert die Variante etwas mehr Programmierdisziplin. Auch kann er deep_const_ptr so ausbauen, dass die Klasse das referenzierte Objekt in seinem Destruktor gleich mitlöscht, während der Entwickler für die Zerstörung von _d selbst sorgen muss. Dafür spielt die d_func()-Variante ihre Stärke in polymorphen Klassenhierarchien aus, wie noch zu zeigen sein wird.

Auf ein weiteres Hindernis stößt der Entwickler, wenn er tatsächlich alle privaten Funktionen von der öffentlichen auf die Private-Klasse verschieben möchte: Ihm fehlt ein Mittel, (nicht statische) öffentliche oder geschützte Methoden der öffentlichen Klasse aus Methoden auf Private heraus aufzurufen, denn die Referenz der öffentlichen Klasse auf ihre Private-Klasse ist unidirektional:

class Class::Private {
public:
Private() : ... {}
// ...
void callPublicFunc() { /*???*/Class::publicFunc(); }
};

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

Das Problem lässt sich durch Einführung eines "Back-Links" umgehen (der gewählte Name q stammt aus dem Qt-Umfeld):

class Class::Private {
Class * const q; // back-link
public:
explicit Private( Class * qq ) : q( qq ), ... {}
// ...
void callPublicFunc() { q->publicFunc(); }
};

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

Beim Verwenden des Back-Links ist jedoch zwingend zu beachten, dass die Initialisierung von Class::d nicht sichergestellt ist, bevor nicht der Private-Konstruktor vollständig durchlaufen wurde. Während der Ausführung des Private-Konstruktors sollte man daher tunlichst den Aufruf solcher (Class-)Methoden vermeiden, die einen gültigen d-Zeiger voraussetzen. Sonst hagelt es Abstürze oder undefiniertes Verhalten.

Der auf Sicherheit besonnene Entwickler initialisiert daher die Back-Links zunächst mit 0 und pflanzt ihnen erst nach der vollständigen Konstruktion die Referenz zur öffentlichen Klasse ein:

class Class::Private {
Class * const q; // back-link
public:
explicit Private( Class * qq ) : q( 0 ), ... {}
// ...
};

Class::Class()
: d( new Private( this ) )
{
// establish back-link:
d->q = this;
}

Trotz der Einschränkungen lässt sich häufig ein erklecklicher Teil des Initialisierungscodes einer Klasse in den Private-Konstruktor verschieben, was bei Klassen mit vielen überladenen Konstruktoren hilfreich ist. Nicht unerwähnt bleiben soll, dass sich auch der q-Zeiger mit deep_const_ptr oder (im Falle von Klassenhierarchien) q_func()-Funktionen zur const-Durchleitung überreden lässt.

Nachdem nun die bisher fehlenden Funktionen nachgerüstet sind, beschäftigt sich der Rest des Artikels damit, den entstandenen Overhead durch einige tiefe Griffe in die Trickkiste etwas abzumildern.