Meine bisherigen Vorgehensweisen bei Bibliotheken waren folgende drei (stark konstruierte, künstliche Beispiele ohne Anspruch auf Vollständigkeit):
Direkt
Die Header-Dateien der implementierenden Klassen sind öffentlich zugänglich.
Code: Alles auswählen
class Window
{
public:
void setSize(int width, int height);
private:
int width;
int height;
};
+ maximale Verwendung des Stacks, keine unnötige Verwendung des Heaps
- private Implementierungsdetails komplett sichtbar
- "Interface" geht schlichtweg unter
Pimpl
Implementierungsdetails sind in einer klasseninternen Struktur verborgen.
Code: Alles auswählen
class Window
{
public:
void setSize(int width, int height);
private:
struct Impl;
Impl *impl; // od. auto_ptr
};
Code: Alles auswählen
struct Window::Impl
{
int height;
int width;
};
Window::Window()
: impl(new Impl)
{
}
Window::~Window()
{
delete impl;
}
void Window::setSize(int width, int height)
{
impl->width = width;
impl->height = height;
}
+ Keine Factories zur Erzeugung nötig
+ Aufrufer muss sich nicht um delete kümmern
- Ziemlich starker Overhead durch Heap und kein direkter Zugriff auf Member
- struct Impl und Impl *impl sind noch im öffentlichen Interface (noch verschmerzbar)
Interface
Der Klassiker mit abstrakten Klassen, die dem Interface-Konzept entsprechen.
Code: Alles auswählen
class IWindow
{
public:
HWND getHandle() const = 0;
virtual ~IWindow() {}
};
IWindow * createWindow(); // Factory-Funktion
Code: Alles auswählen
class Window : public IWindow
{
public:
HWND getHandle() const;
private:
HWND handle;
};
// .CPP
HWND Window::getHandle() const
{
return handle;
}
Code: Alles auswählen
class IApplicationWindow : public IWindow
{
public:
// ...
virtual ~IApplicationWindow() {}
};
IApplicationWindow * createApplicationWindow();
Code: Alles auswählen
class ApplicationWindow : public IApplicationWindow : public Window
{
public:
HWND getHandle() const; // Macht keinen Spaß!
};
// .CPP
HWND ApplicationWindow::getHandle() const // Wirklich nicht! :-(
{
return Window::getHandle();
}
- Factories
- Wieder spielt sich eigentlich alles im Heap ab
- Aufrufer muss sich um delete kümmern
- Sehr unschöner Programmier-Overhead wenn öffentliche Schnittstellen voneinander erben (siehe oben)
Resumée
Schnellste Lösung ist die direkte Methode, keine Frage. Es macht aber keinen Spaß eine solche Bibliothek zu verwenden.
Die beiden anderen Methoden werkeln auf dem im Vergleich zum Stack lahmen Heap rum.
Interfaces übetragen dem Aufrufer eine unnötig hohe Verantwortung (delete).
Vorteile durch C++0x
Die Factories könnten shared_ptrs zurückgeben. Kein delete vom Aufrufer mehr nötig. Factories an sich sind auch nicht unsauber. Wem das nicht gefällt, soll eine rein objektorientierte Sprache nutzen, der Mix aus prozeduraler und objektorientierter Programmierung ist meiner Meinung nach einer der größten Vorteile von C++ den man nutzen sollte! Es fördert sogar die Kapslung (siehe einschlägige Literatur von Sutter/Herb). Trotzdem bleibt das Problem der Schnittstellenvererbung. Das macht keinen Spaß zu programmieren durch die ganzen Weiterleitungen der eigentlichen Methodenaufrufe an die Basisklassen. Immerhin bekommt der Bibliotheksnutzer nichts davon mit.
Bei den Impls könnte/sollte man nun den unique_ptr verwenden, aber es ändert glaube ich nichts an dem Umweg für jeden Member.
Ich habe die ganze Zeit das Gefühl, etwas grundlegendes noch zu übersehen, dass das ganze vereinfachen würde. Vorteile mit der Move-Semantik bei den Factories gibt es ja nicht, weil ich durch die abstrakten Schnittstellen Zeiger zurück geben muss. Oder gibt es da eine clevere Alternative? Ich würde ungern so viel auf dem Heap arbeiten.
Habt ihr noch Ideen, Tipps & Tricks oder Anregungen?