Hinweis: Das Folgende ist C ++ 03-Code, wir erwarten jedoch einen Wechsel zu C ++ 11 in den nächsten zwei Jahren, daher müssen wir dies berücksichtigen.
Ich schreibe eine Anleitung (unter anderem für Anfänger), wie man eine abstrakte Oberfläche in C ++ schreibt. Ich habe beide Artikel von Sutter zu diesem Thema gelesen, im Internet nach Beispielen und Antworten gesucht und einige Tests durchgeführt.
Dieser Code darf NICHT kompiliert werden!
void foo(SomeInterface & a, SomeInterface & b)
{
SomeInterface c ; // must not be default-constructible
SomeInterface d(a); // must not be copy-constructible
a = b ; // must not be assignable
}
Alle oben genannten Verhaltensweisen finden die Ursache für ihr Problem in der Aufteilung : Die abstrakte Schnittstelle (oder Nicht-Blatt-Klasse in der Hierarchie) sollte weder konstruierbar noch kopierbar / zuweisbar sein, AUCH wenn die abgeleitete Klasse es kann.
0. Lösung: die Grundschnittstelle
class VirtuallyDestructible
{
public :
virtual ~VirtuallyDestructible() {}
} ;
Diese Lösung ist einfach und etwas naiv: Sie erfüllt nicht alle unsere Einschränkungen: Sie kann standardmäßig erstellt, kopiert und kopiert zugewiesen werden (ich bin mir nicht einmal sicher, ob Konstruktoren und Zuweisungen verschoben werden müssen, aber ich habe noch 2 Jahre Zeit, um sie abzubilden es raus).
- Wir können den Destruktor nicht als rein virtuell deklarieren, da wir ihn inline halten müssen und einige unserer Compiler keine reinen virtuellen Methoden mit inline leerem Body verarbeiten können.
- Ja, der einzige Punkt dieser Klasse ist, die Implementierer praktisch zerstörbar zu machen, was ein seltener Fall ist.
- Selbst wenn wir eine zusätzliche virtuelle reine Methode hätten (was in den meisten Fällen der Fall ist), wäre diese Klasse immer noch kopierzuweisbar.
Also nein ...
1. Lösung: boost :: noncopyable
class VirtuallyDestructible : boost::noncopyable
{
public :
virtual ~VirtuallyDestructible() {}
} ;
Diese Lösung ist die beste, weil sie einfach, klar und C ++ (keine Makros) ist.
Das Problem ist, dass es für diese spezifische Schnittstelle immer noch nicht funktioniert, da VirtuallyConstructible immer noch standardmäßig erstellt werden kann .
- Wir können den Destruktor nicht als rein virtuell deklarieren, da wir ihn inline halten müssen und einige unserer Compiler ihn nicht verarbeiten können.
- Ja, der einzige Punkt dieser Klasse ist, die Implementierer praktisch zerstörbar zu machen, was ein seltener Fall ist.
Ein weiteres Problem besteht darin, dass Klassen, die die nicht kopierbare Schnittstelle implementieren, dann den Kopierkonstruktor und den Zuweisungsoperator explizit deklarieren / definieren müssen, wenn sie diese Methoden benötigen (und in unserem Code haben wir Wertklassen, auf die unser Client weiterhin über zugreifen kann Schnittstellen).
Dies verstößt gegen die Nullregel, nach der wir vorgehen möchten: Wenn die Standardimplementierung in Ordnung ist, sollten wir sie verwenden können.
2. Lösung: Schützen Sie sie!
class MyInterface
{
public :
virtual ~MyInterface() {}
protected :
// With C++11, these methods would be "= default"
MyInterface() {}
MyInterface(const MyInterface & ) {}
MyInterface & operator = (const MyInterface & ) { return *this ; }
} ;
Dieses Muster folgt den technischen Einschränkungen, die wir hatten (zumindest im Benutzercode): MyInterface kann nicht standardmäßig erstellt, nicht kopiert und nicht kopiert werden.
Außerdem werden der Implementierung von Klassen keine künstlichen Einschränkungen auferlegt. Sie können dann entweder die Nullregel befolgen oder einige Konstruktoren / Operatoren in C ++ 11/14 problemlos als "= default" deklarieren.
Das ist ziemlich ausführlich, und eine Alternative wäre die Verwendung eines Makros, so etwas wie:
class MyInterface
{
public :
virtual ~MyInterface() {}
protected :
DECLARE_AS_NON_SLICEABLE(MyInterface) ;
} ;
Der protected muss außerhalb des Makros bleiben (da er keinen Gültigkeitsbereich hat).
Richtig "namespaced" (dh mit dem Namen Ihres Unternehmens oder Produkts vorangestellt) sollte das Makro harmlos sein.
Und der Vorteil ist, dass der Code in einer Quelle berücksichtigt wird, anstatt in alle Schnittstellen kopiert zu werden. Sollte der move-Konstruktor und die move-Zuweisung in Zukunft explizit auf die gleiche Weise deaktiviert werden, wäre dies eine sehr leichte Änderung im Code.
Fazit
- Bin ich paranoid, wenn der Code vor dem Durchschneiden von Schnittstellen geschützt werden soll? (Ich glaube nicht, aber man weiß es nie ...)
- Was ist die beste Lösung unter den oben genannten?
- Gibt es eine andere, bessere Lösung?
Bitte denken Sie daran, dass dies ein Muster ist, das (unter anderem) als Richtlinie für Neulinge dient. Daher ist eine Lösung wie "Jeder Fall sollte implementiert werden" keine praktikable Lösung.
Kopfgeld und Ergebnisse
Ich habe das Kopfgeld an coredump vergeben, weil ich Zeit für die Beantwortung der Fragen und die Relevanz der Antworten aufgewendet habe.
Meine Lösung des Problems wird wahrscheinlich in etwa so aussehen:
class MyInterface
{
DECLARE_CLASS_AS_INTERFACE(MyInterface) ;
public :
// the virtual methods
} ;
... mit folgendem Makro:
#define DECLARE_CLASS_AS_INTERFACE(ClassName) \
public : \
virtual ~ClassName() {} \
protected : \
ClassName() {} \
ClassName(const ClassName & ) {} \
ClassName & operator = (const ClassName & ) { return *this ; } \
private :
Dies ist eine praktikable Lösung für mein Problem aus den folgenden Gründen:
- Diese Klasse kann nicht instanziiert werden (die Konstruktoren sind geschützt)
- Diese Klasse kann praktisch zerstört werden
- Diese Klasse kann vererbt werden, ohne dass der Vererbung von Klassen übermäßige Einschränkungen auferlegt werden (z. B. kann die vererbende Klasse standardmäßig kopiert werden).
- Die Verwendung des Makros bedeutet, dass die "Deklaration" der Schnittstelle leicht erkennbar (und durchsuchbar) ist und der Code an einer Stelle berücksichtigt wird, was die Änderung erleichtert (ein geeigneter vorangestellter Name entfernt unerwünschte Namenskonflikte).
Beachten Sie, dass die anderen Antworten wertvolle Erkenntnisse lieferten. Vielen Dank an alle, die es probiert haben.
Beachten Sie, dass ich denke, dass ich dieser Frage noch ein Kopfgeld hinzufügen kann, und ich schätze aufschlussreiche Antworten so sehr, dass ich ein Kopfgeld eröffnen würde, um es dieser Antwort zuzuweisen.
virtual ~VirtuallyDestructible() = 0
virtuelle Vererbung von Schnittstellenklassen (nur mit abstrakten Mitgliedern). Sie könnten das VirtuallyDestructible wahrscheinlich weglassen.
virtual void bar() = 0;
beispielsweise? Das würde verhindern, dass Ihre Schnittstelle instanziiert wird.