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 void bar() = 0;
beispielsweise? Das würde verhindern, dass Ihre Schnittstelle instanziiert wird.virtual ~VirtuallyDestructible() = 0
virtuelle Vererbung von Schnittstellenklassen (nur mit abstrakten Mitgliedern). Sie könnten das VirtuallyDestructible wahrscheinlich weglassen.Antworten:
Die kanonische Möglichkeit, eine Schnittstelle in C ++ zu erstellen, besteht darin, ihr einen reinen virtuellen Destruktor zu geben. Dies stellt sicher, dass
delete
eines Zeigers auf die Schnittstelle macht das Richtige: Es ruft den Destruktor der am häufigsten abgeleiteten Klasse für diese Instanz auf.Nur ein reiner virtueller Destruktor verhindert nicht die Zuweisung eines Verweises auf die Schnittstelle. Wenn dies ebenfalls fehlschlagen soll, müssen Sie Ihrer Schnittstelle einen geschützten Zuweisungsoperator hinzufügen.
Jeder C ++ - Compiler sollte in der Lage sein, eine Klasse / Schnittstelle wie diese zu verarbeiten (alles in einer Header-Datei):
Wenn Sie einen Compiler haben, der dies unterdrückt (was bedeutet, dass es sich um einen Compiler vor C ++ 98 handelt), ist Ihre Option 2 (mit geschützten Konstruktoren) eine gute zweitbeste.
Die Verwendung
boost::noncopyable
ist für diese Aufgabe nicht empfehlenswert, da sie die Meldung sendet, dass alle Klassen in der Hierarchie nicht kopierbar sein sollten, und daher Verwirrung bei erfahreneren Entwicklern stiften kann, die mit Ihren Absichten für eine solche Verwendung nicht vertraut sind.quelle
If you need [prevent assignment] to fail as well, then you must add a protected assignment operator to your interface.
: Das ist die Wurzel meines Problems. Die Fälle, in denen ich eine Schnittstelle benötige, um die Zuweisung zu unterstützen, müssen in der Tat selten sein. Andererseits sind die Fälle, in denen ich eine Schnittstelle als Referenz übergeben möchte (die Fälle, in denen NULL nicht akzeptabel ist) und daher ein No-Op oder ein Slicing, das kompiliert wird, vermeiden möchten, viel größer.private
? Darüber hinaus möchten Sie sich möglicherweise mit dem Standard- und Kopier-ctor befassen.Bin ich paranoid ...
Ist das nicht ein Risikomanagement-Problem?
Beste Lösung
Ihre zweite Lösung ("machen Sie sie geschützt") sieht gut aus, aber denken Sie daran, dass ich kein C ++ - Experte bin.
Zumindest scheinen die ungültigen Verwendungen von meinem Compiler (g ++) als fehlerhaft gemeldet worden zu sein.
Benötigen Sie nun Makros? Ich würde "Ja" sagen, denn obwohl Sie nicht sagen, was der Zweck der Richtlinie ist, die Sie schreiben, besteht diese meiner Meinung nach darin, bestimmte Best Practices im Code Ihres Produkts durchzusetzen.
Zu diesem Zweck können Makros helfen, festzustellen, wann die Benutzer das Muster effektiv anwenden: Ein grundlegender Filter für Festschreibungen kann Ihnen mitteilen, ob das Makro verwendet wurde:
protected
Schlüsselwort gibt).Ohne Makros müssen Sie überprüfen, ob das Muster in allen Fällen notwendig und gut implementiert ist.
Bessere Lösung
Das Schneiden in C ++ ist nichts anderes als eine Besonderheit der Sprache. Da Sie eine Richtlinie schreiben (insbesondere für Anfänger), sollten Sie sich auf das Unterrichten konzentrieren und nicht nur "Codierungsregeln" aufzählen. Sie müssen sicherstellen, dass Sie wirklich erklären, wie und warum das Schneiden stattfindet, zusammen mit Beispielen und Übungen (erfinden Sie das Rad nicht neu, lassen Sie sich von Büchern und Tutorials inspirieren).
Der Titel einer Übung könnte beispielsweise lauten: " Wie lautet das Muster für eine sichere Schnittstelle in C ++ ?"
Ihr bester Schritt wäre es also, sicherzustellen, dass Ihre C ++ - Entwickler verstehen, was passiert, wenn das Schneiden stattfindet. Ich bin davon überzeugt, dass sie in diesem Fall nicht so viele Fehler im Code machen, wie Sie befürchten würden, auch ohne dieses bestimmte Muster formal durchzusetzen (aber Sie können es trotzdem durchsetzen, Compiler-Warnungen sind gut).
Über den Compiler
Du sagst :
Oft werden die Leute sagen "Ich habe nicht das Recht, [X] zu tun" , "Ich soll nicht [Y] tun ..." , ... weil sie denken, dass dies nicht möglich ist und nicht, weil sie es tun versucht oder gefragt.
Es ist wahrscheinlich Teil Ihrer Stellenbeschreibung, Ihre Meinung zu technischen Fragen zu äußern. Wenn Sie der Meinung sind, dass der Compiler die perfekte (oder einzigartige) Wahl für Ihre Problemdomäne ist, verwenden Sie ihn. Sie sagten aber auch, dass "reine virtuelle Destruktoren mit Inline-Implementierung nicht der schlechteste Erstickungspunkt sind, den ich je gesehen habe" . Nach meinem Verständnis ist der Compiler so speziell, dass selbst erfahrene C ++ - Entwickler Schwierigkeiten haben, ihn zu verwenden: Ihr älterer / interner Compiler ist jetzt technisch hoch verschuldet, und Sie haben das Recht (die Pflicht?), dieses Problem mit anderen Entwicklern und Managern zu diskutieren .
Versuchen Sie, die Kosten für die Beibehaltung des Compilers im Vergleich zu den Kosten für die Verwendung eines anderen Compilers zu bewerten:
Ich kenne Ihre Situation nicht, und tatsächlich haben Sie wahrscheinlich triftige Gründe, an einen bestimmten Compiler gebunden zu sein.
Aber in dem Fall, dass dies nur eine reine Trägheit ist, wird sich die Situation niemals ändern, wenn Sie oder Ihre Mitarbeiter keine Probleme mit der Produktivität oder der technischen Verschuldung melden.
quelle
Am I paranoid...
: "Machen Sie die Verwendung Ihrer Schnittstellen einfach und fehlerhaft." Ich habe dieses spezielle Prinzip probiert, als jemand meldete, dass eine meiner statischen Methoden versehentlich falsch angewendet wurde. Der erzeugte Fehler schien nichts damit zu tun zu haben, und es dauerte mehrere Stunden, bis ein Ingenieur die Quelle gefunden hatte. Dieser "Schnittstellenfehler" entspricht dem Zuweisen einer Schnittstellenreferenz zu einer anderen. Also, ja, ich möchte diese Art von Fehler vermeiden. Außerdem besteht die Philosophie in C ++ darin, zur Kompilierungszeit so viel wie möglich abzufangen, und die Sprache gibt uns diese Kraft, also gehen wir damit um.Best solution
: Genau. . .Better solution
: Das ist eine tolle Antwort. Ich werde daran arbeiten ... Nun zum ThemaPure virtual classes
: Was ist das? Eine abstrakte C ++ Schnittstelle? (Klasse ohne Status und nur rein virtuelle Methoden?). Wie hat mich diese "rein virtuelle Klasse" vor dem Aufschneiden geschützt? (Bei rein virtuellen Methoden wird die Instanziierung nicht kompiliert, aber die Zuweisung von Kopien und die Zuweisung von Verschiebungen wird ebenfalls IIRC).About the compiler
: Wir sind uns einig, aber unsere Compiler liegen außerhalb meines Verantwortungsbereichs (nicht, dass es mich von pingeligen Kommentaren abhält ... :-p ...). Ich werde die Details nicht weitergeben (ich wünschte, ich könnte es), aber es hängt mit internen Gründen (wie Testsuiten) und externen Gründen (z. B. Verlinkung des Clients mit unseren Bibliotheken) zusammen. Letztendlich ist das Ändern der Compilerversion (oder sogar das Patchen) KEINE triviale Operation. Geschweige denn einen kaputten Compiler durch einen neuen gcc ersetzen.Das Slicing-Problem ist eines, aber sicherlich nicht das einzige, das auftritt, wenn Sie Ihren Benutzern eine polymorphe Laufzeitschnittstelle zur Verfügung stellen. Denken Sie an Nullzeiger, Speicherverwaltung und gemeinsame Daten. Keines dieser Probleme lässt sich in jedem Fall leicht lösen (intelligente Zeiger sind großartig, aber selbst sie sind keine Wunderwaffe). Tatsächlich scheint es in Ihrem Beitrag nicht so zu sein , als wollten Sie das Problem des Aufteilens lösen , sondern umgehen es, indem Sie Benutzern nicht erlauben, Kopien zu erstellen. Alles, was Sie tun müssen, um eine Lösung für das Slicing-Problem anzubieten, ist das Hinzufügen einer Funktion für virtuelle Klonmitglieder. Ich denke, das tiefere Problem beim Anzeigen einer polymorphen Laufzeitschnittstelle besteht darin, dass Sie Benutzer dazu zwingen, sich mit Referenzsemantik zu befassen, die schwerer zu erklären ist als Wertsemantik.
Der beste Weg, wie ich weiß, um diese Probleme in C ++ zu vermeiden, ist die Typlöschung . Dies ist eine Technik, bei der Sie eine polymorphe Laufzeitschnittstelle hinter einer normalen Klassenschnittstelle verbergen. Diese normale Klassenschnittstelle hat dann eine Wertesemantik und kümmert sich um alles polymorphe "Durcheinander" hinter den Bildschirmen.
std::function
ist ein Paradebeispiel für das Löschen von Typen.Die folgenden Präsentationen von Sean Parent erläutern, warum es schlecht ist, die Vererbung Ihren Benutzern zugänglich zu machen und wie das Löschen von Typen Abhilfe schaffen kann:
Vererbung ist die Basisklasse des Bösen (kurze Version)
Value Semantics und Concepts-based Polymorphism (lange Version; einfacher zu folgen, aber der Klang ist nicht großartig)
quelle
Du bist nicht paranoid. Meine erste berufliche Aufgabe als C ++ - Programmierer führte zum Absturz. Ich kenne andere. Dafür gibt es nicht viele gute Lösungen.
In Anbetracht Ihrer Compiler-Einschränkungen ist Option 2 die beste. Anstatt ein Makro zu erstellen, das Ihre neuen Programmierer als seltsam und mysteriös ansehen werden, würde ich ein Skript oder Tool zur automatischen Codegenerierung vorschlagen. Wenn Ihre neuen Mitarbeiter eine IDE verwenden, sollten Sie in der Lage sein, ein "New MYCOMPANY Interface" -Tool zu erstellen, das nach dem Namen der Schnittstelle fragt und die gewünschte Struktur erstellt.
Wenn Ihre Programmierer die Befehlszeile verwenden, verwenden Sie die für Sie verfügbare Skriptsprache, um das Skript NewMyCompanyInterface zum Generieren des Codes zu erstellen.
Ich habe diesen Ansatz in der Vergangenheit für gängige Codemuster (Schnittstellen, Zustandsautomaten usw.) verwendet. Das Schöne daran ist, dass neue Programmierer die Ausgabe leicht lesen und verstehen können und den benötigten Code reproduzieren können, wenn sie etwas benötigen, das nicht generiert werden kann.
Makros und andere Methoden der Metaprogrammierung verschleiern in der Regel das Geschehen, und die neuen Programmierer erfahren nicht, was "hinter den Kulissen" geschieht. Wenn sie das Muster brechen müssen, sind sie genauso verloren wie zuvor.
quelle