Interfaces
Die einfachste Variante, die wohl auch weithin bekannt ist, ist das Ersetzen öffentlicher Klassendefinitionen durch rein virtuelle Schnittstellendefinitionen. Die am wenigsten redundante Implementierung sieht in diesem Fall Java-Modulen sehr ähnlich:
Code: Alles auswählen
// header (*.h)
class pimpl_interface
{
// delete assignment operator
pimpl_interface& operator =(const pimpl_interface&); // C++11: = delete;
public:
virtual ~pimpl_interface() { }
};
class foo : public pimpl_interface
{
public:
// public methods
virtual void publicMethod() = 0;
};
std::unique_ptr<foo> createFoo(...);
// implementation (*.cpp)
class fooImpl : public foo
{
// attributes
int i;
// private methods
void privateMethod(int &i)
{
++i;
}
public:
// public methods
void publicMethod()
{
privateMethod(i);
++i;
}
};
std::unique_ptr<foo> createFoo(...)
{
return unique_ptr<foo>( new foo(...) );
}
Doch auch die Nachteile liegen auf der Hand: Alle Objekte landen im Freispeicher. Eine extra create-Funktion ist erforderlich, die Liste der Konstruktorparameter muss doppelt angegeben werden und in der create-Funktion müssen alle Parameter von Hand an den Konstruktor weitergegeben werden. Insbesondere helfen hier auch in C++11 keine Variadic Templates, weil die create-Funktion nicht im Header definiert werden kann.
Obendrein sind alle öffentlichen Methoden virtuell und haben somit einen (wenn auch sehr sehr geringen, also in der Regel zu vernachlässigenden) Mehraufwand. Entscheidend ist hier, dass der Compiler Methoden ggf. zur Link-Zeit nicht inlinen kann, die er ohne die Indirektion beim virtuellen Aufruf durchaus inlinen könnte. Nebenbei ist hier Erweiterung durch Vererbung kaum sinnvoll möglich.
Private Methoden
In vielen Fällen reicht es aus, sich klar zu werden, dass private Funktionalität überhaupt nicht in die Klasse muss, sondern ganz einfach in modulinternen Funktionen implementiert werden kann:
Code: Alles auswählen
// header (*.h)
class foo
{
public:
// public methods
void publicMethod();
private:
int i;
};
// implementation (*.cpp)
namespace
{
// private functions ("methods")
void privateMethod(int &i)
{
++i;
}
} // namespace
// public methods
void foo::publicMethod()
{
privateMethod(i);
++i;
}
Diese erste gezeigte Form der Umwandlung privater Methoden in private Funktionen ist sowohl einfach als auch sauber. Insbesondere wird durch die explizite Übergabe privater Attribute auch auf Aufruferseite deutlich, welche Attribute die jeweilige private Funktion tatsächlich verändert (keine versteckten Nebenwirkungen). Ein Teil der privaten Funktionen ist dank der vollständigen Entkopplung von der jeweiligen Klasse möglicherweise sogar außerhalb des Moduls nützlich, und kann somit in Bibliotheksfunktionen übergehen.
Leider gibt es Fälle, in denen die Einzelübergabe privater Attribute an private Funktionen alleine aufgrund der großen Attributanzahl äußerst mühsam und damit unpraktikabel wird. In diesem Fall kann auf eine verschachtelte Attributstruktur zurückgegriffen werden, deren Typ öffentlich zugänglich, die Instanz selbst jedoch privat ist:
Code: Alles auswählen
// header (*.h)
class foo
{
public:
foo(...);
// public methods
void publicMethod();
struct M
{
// attributes
int sth;
int sthElse;
M(...);
};
private:
M m;
};
// implementation (*.cpp)
foo::M::M(...)
: sth(0),
sthElse(1)
{
// consistent member access
M &m = *this;
m.sth = ...;
// ...
}
foo::foo(...)
: m(...)
{
}
// private methods
void privateMethod(foo::M &m)
{
m.sth = ...;
}
// public methods
void foo::publicMethod()
{
privateMethod(m);
m.sthElse = ...;
}
Ein anonymer namespace ist hier nicht zwingend erforderlich, weil die Signatur durch den Parametertyp foo::M stets eindeutig einer Klasse und damit einem implementierenden Modul konfliktfrei zuzuordnen ist.
Ein unschöner Nachteil der zweiten gezeigten Variante ist, dass die Konstruktorparameterliste wie bei Interfaces (siehe oben) zweimal angegeben werden muss, hier einmal für foo und einmal für foo::M. Zudem müssen auch hier die Parameter im Konstruktor von foo von Hand an foo::M weitergegeben werden. Mit C++11 lässt sich dieses Problem lösen, in MSVC++ müssen wir aber leider noch auf die Unterstützung von Variadic Templates warten:
Code: Alles auswählen
// header (*.h)
class foo
{
public:
template<typename ...Args>
foo(Args&& ...args) : m(std::forward<Args>(args)...) { }
// public methods
void publicMethod();
struct M
{
// attributes
int sth;
int sthElse;
M(...);
};
private:
M m;
};
// implementation (*.cpp)
foo::M::M(...)
: sth(0),
sthElse(1)
{
// consistent member access
M &m = *this;
m.sth = ...;
// ...
}
// private methods
void privateMethod(foo::M &m)
{
m.sth = ...;
}
// public methods
void foo::publicMethod()
{
privateMethod(m);
m.sthElse = ...;
}
Code: Alles auswählen
// header (*.h)
class foo
{
public:
// public methods
void publicMethod();
private:
// attributes
int sth;
int sthElse;
struct M;
};
// implementation (*.cpp)
struct foo::M
{
// private methods
static void privateMethod(foo &m)
{
m.sth = ...;
}
};
// public methods
void foo::publicMethod()
{
M::privateMethod(*this);
sthElse = ...;
}
Manchmal sollen nicht nur private Methoden, sondern auch private Attribute nach Außen nicht sichtbar sein. (Eigentlich fast immer, schon um Abhängigkeiten zu anderen Modulen zu vermeiden. Leider lassen sich private Attribute praktisch nicht elegant ohne Overhead verstecken, weswegen am Ende eine Abwägung zwischen Vermeidung von Abhängigkeiten und Vermeidung von Overhead steht.)
Etwas besseres als das PImpl-Muster (neben Interfaces, siehe oben) habe ich hier auch nicht finden können:
Code: Alles auswählen
// header (*.h)
class foo
{
public:
foo(...);
~foo();
// public methods
void publicMethod();
struct M;
private:
std::unique_ptr<M> m; // bonus: use wrapper to enforce initialization, see http://herbsutter.com/gotw/_101/
};
// implementation (*.cpp)
struct foo::M
{
// attributes
int sth;
int sthElse;
M(...)
: sth(0),
sthElse(1)
{
// consistent member access
M &m = *this;
m.sth = ...;
// ...
}
};
foo::foo(...)
: m( new M(...) )
{
}
// required, foo::M incomplete outside implementation file
foo::~foo()
{
}
// private methods
void privateMethod(foo::M &m)
{
m.sth = ...;
}
// public methods
void foo::publicMethod()
{
privateMethod(*m);
m->sthElse = ...;
}
// alternative: consistent member access
void foo::publicMethod()
{
M &m = *this;
privateMethod(m);
m.sthElse = ...;
}
Übrigens hat M &m = *this; nicht nur Vorteile in Bezug auf Einheitlichkeit (d.h. keine Veränderung des Codes bei Auslagerung in private Funktionen), sondern auch in Bezug auf Effizienz: Da sich lokale Referenzen nie ändern, kann der Compiler auch nach dem Aufruf anderer Funktionen über das lokale m referenzierte Attribute weiterhin direkt dereferenzieren, ohne noch einmal die Adresse von m aus this->m lesen zu müssen. Damit ist der Zugriff auf PImpl-Attribute genauso effizient wie der Zugriff auf Attribute über this.
Um die Zahl der Fehlerquellen zu minimieren und das PImpl-Muster weitestgehend zu automatisieren, empfiehlt sich ein entsprechender Wrapper. Herb Sutters PImpl-Wrapper ist hierfür der richtige Ausgangspunkt.
Das PImpl-Muster teilt sich einige Nachteile mit der eingangs vorgestellten Interface-Variante: Zwar ist die Benutzung der PImpl-Klasse von außen natürlicher, weil die Standard-Objektkonstruktions- und Destruktionsmechanismen, insbesondere automatische Lebenszeit ohne Zeiger-Wrapper, genutzt werden können. Intern landet das Objekt jedoch in beiden Fällen schlussendlich im Freispeicher, die PImpl-Klasse hat die Verwaltung des entsprechenden Objektes lediglich bereits mit eingebaut. Auch die Redundanz bei der Konstruktion wird man intern nicht los, neben den öffentlichen Konstruktoren ist meist mindestens ein zusätzlicher interner Konstruktor der privaten Implementierung notwendig (es sei denn, man verzichtet auf Initialisierung in der Initialisierungsliste).
Vorteile der PImpl-Variante: Im Gegensatz zur Interface-Variante erlaubt die PImpl-Variante Erweiterung durch Vererbung. Zudem sind öffentliche Methoden nicht virtuell. Damit verschwinden zum einen Indirektionen beim Aufruf, viel wichtiger ist jedoch, dass der Compiler in diesem Fall zur Link-Zeit Methoden inlinen kann.
Fazit
So weit einige Skizzen zu den Implementierungsmustern, die ich heute verwende, um die Verfehlungen von C++ in Bezug auf das Modulsystem zu lindern. Schön ist, dass einige dieser Muster, insbesondere private Funktionen, sogar die Produktivität steigern können. Andere Muster, wie das PImpl-Muster, machen das Programmieren unweigerlich mindestens ein bisschen umständlicher, ich hoffe aber, dass ich mit den hier vorgestellten Varianten diese Umstände bestmöglich minimieren konnte.
Nachtrag 1: Konstrastierung von Interfaces und PImpl-Muster.
Nachtrag 2: Private Funktionen mit Nebenwirkungen ohne Verpacken der Attribute möglich, entsprechend nachgetragen.