2013-08-11 11 views
7

Move-Semantik eignet sich hervorragend für RAII-Klassen. Sie erlauben es einem, so zu programmieren, als hätte man eine Wertesemantik ohne die Kosten von schweren Kopien. Ein gutes Beispiel hierfür ist returning std::vector from a function. Programmieren mit Wert-Semantik bedeutet jedoch, dass man erwarten würde, dass sich Typen wie primitive Datentypen verhalten. Diese beiden Aspekte scheinen sich manchmal zu widersprechen.Was sollte der Standardkonstruktor in einer RAII-Klasse mit Bewegungssemantik tun?

Zum einen würde man in RAII erwarten, dass der Standardkonstruktor ein vollständig initialisiertes Objekt zurückgibt oder eine Ausnahme auslöst, wenn die Ressourcenerfassung fehlgeschlagen ist. Dies garantiert, dass jedes konstruierte Objekt in einem gültigen und konsistenten Zustand ist (d. H. Sicher zu verwenden).

Auf der anderen Seite gibt es mit Bewegungssemantik einen Punkt, wenn Objekte in einem valid but unspecified state sind. In ähnlicher Weise können sich primitive Datentypen in einem nicht initialisierten Zustand befinden. Daher mit Wertesemantik, würde ich den Standardkonstruktor erwartet ein Objekt in dieser gültig, aber nicht spezifizierten Zustand zu schaffen, so dass der folgende Code das erwartete Verhalten haben würde:

// Primitive Data Type, Value Semantics 
int i; 
i = 5; 

// RAII Class, Move Semantics 
Resource r; 
r = Resource{/*...*/} 

In beiden Fällen würde ich erwarten, dass die " schwere "Initialisierung nur einmal auftreten. Ich frage mich, was ist die beste Vorgehensweise dabei? Offensichtlich gibt es ein kleines praktisches Problem mit dem zweiten Ansatz: Wenn der Standardkonstruktor Objekte im unspezifizierten Zustand erzeugt, wie würde man einen Konstruktor schreiben, der eine Ressource annimmt, aber keine zusätzlichen Parameter nimmt? (Das Senden von Tags kommt mir in den Sinn ...)

Bearbeiten: Einige der Antworten haben die Logik in Frage gestellt, dass Ihre Klassen wie primitive Datentypen funktionieren sollen. Ein Teil meiner Motivation kommt von Alexander Stepanov's Efficient Programming with Components, wo er über normale Typen redet. Insbesondere möchte ich folgendes Zitat nennen:

Was auch immer ein natürlicher idiomatischer Ausdruck in c [für eingebaute Typen] ist, sollte ein natürlicher idiomatischer Ausdruck für reguläre Typen sein.

Er fährt fort, fast das gleiche Beispiel wie oben zu liefern. Ist dieser Punkt in diesem Zusammenhang nicht gültig? Verstehe ich das falsch?

Bearbeiten: Da es nicht viel Diskussion gegeben hat, bin ich im Begriff, die am höchsten gewählte Antwort zu akzeptieren. Es ist wahrscheinlich keine gute Idee, Objekte im Status "bewegt aus wie" im Standardkonstruktor zu initialisieren, da jeder, der mit den vorhandenen Antworten einverstanden war, dieses Verhalten nicht erwarten würde.

+1

Es gibt verschiedene Arten von RAII-Klassen: Es gibt solche, die Parameter zur Beschreibung der Ressource benötigen, die sie erwerben, und solche, die dies nicht tun. Welches ist deins? – Yakk

+0

Die RAII-Klassen, die ich schreibe, sind meist Wrapper um C-Bibliotheken wie OpenGL, SDL oder FFmpeg. Manchmal erfordert das Erfassen von Ressourcen Parameter und manchmal nicht. Ich wollte das Verhalten des Standardkonstruktors für alle Wrapper allgemein definieren (um mögliche Verwirrung zu minimieren). – kloffy

Antwort

6

Programmieren mit Wert Semantik bedeutet jedoch, dass man erwarten würde, Typen wie primitive Datentypen zu verhalten.

Stichwort "wie". Nicht "identisch mit".

daher mit Wert Semantik, würde ich der Standard- Konstruktor erwartet ein Objekt in dieser gültig, aber nicht spezifizierten Zustand

ich wirklich nicht sehen, zu erstellen, warum Sie das erwarten. Es scheint mir kein sehr wünschenswertes Merkmal zu sein.

Was ist die beste Vorgehensweise in diesem Zusammenhang?

Vergessen Sie diese Idee, dass eine nicht-POD-Klasse diese Funktion mit primitiven Datentypen gemeinsam haben sollte. Es ist falsch geleitet. Wenn es keine vernünftige Möglichkeit gibt, eine Klasse ohne Parameter zu initialisieren, sollte diese Klasse keinen Standardkonstruktor haben.

Wenn Sie ein Objekt deklarieren möchten, aber nicht initialisiert (vielleicht in einem tieferen Bereich), dann verwenden Sie std::unique_ptr.

+0

+1 für die Erwähnung von 'unique_ptr'; Ich habe es für die faule Initialisierung nützlich gefunden. –

+0

Ich denke, Ihre Antwort ist sehr valide und das ist eine praktikable Möglichkeit, das Problem anzugehen. Ich denke jedoch, dass einige Leute anderer Meinung sind. Siehe beispielsweise Alexander Stepanov über reguläre Typen (https://www.youtube.com/watch?v=DOoO7_yvjQE&t=14m00s). – kloffy

+0

Eine kurze Zusammenfassung meines Verständnisses von dem, was Alexander Stepanov in dem vorher verlinkten Video sagt: Er hat STL entworfen, um mit Typen zu arbeiten, die * wie * eingebaute Typen sind (und er fährt fort, über die grundlegenden Operationen zu sprechen). Also, ich denke, es kann von Wert sein, wenn diese grundlegenden Operationen Ihres Typs sich wie ein primitiver Datentyp verhalten. – kloffy

5

Wenn Sie akzeptieren, dass Objekte generell durch Konstruktion gültig sein sollten und alle möglichen Operationen auf einem Objekt nur zwischen gültigen Zuständen verschieben sollten, dann scheint es mir, dass Sie mit einem Standardkonstruktor nur einen von zweien sagen Dinge:

  • Dieser Wert ist ein Container oder ein anderes Objekt mit einem vernünftigen „leeren“ Zustand, den ich zu mutieren-eg beabsichtige, std::vector.

  • Dieser Wert hat keine Elementvariablen und wird hauptsächlich für seinen Typ verwendet, z. B. std::less.

Daraus folgt nicht, dass ein fahrener von Objekt müssen notwendigerweise den gleichen Zustand wie ein Standard-konstruiert haben ein. Beispielsweise könnte ein std::string, der die leere Zeichenfolge "" enthält, einen anderen Status als eine von string verschobene Instanz haben. Wenn Sie ein Objekt standardmäßig konstruieren, erwarten Sie, damit zu arbeiten; Wenn du dich von einem Objekt bewegst, zerstörst du es die meiste Zeit einfach.

Wie würde man einen Konstruktor schreiben, der eine Ressource annimmt, aber keine zusätzlichen Parameter nimmt?

Wenn Ihr Standard-Konstruktor teuer ist und keine Parameter benötigt, würde ich fragen, warum. Sollte es wirklich etwas so teures machen? Woher kommen seine Standardparameter? Einige globale Konfigurationen? Vielleicht wäre es einfacher, sie explizit zu übergeben. Nehmen Sie das Beispiel std::ifstream: mit einem Parameter öffnet sein Konstruktor eine Datei; Ohne verwenden Sie die open() Elementfunktion.

+0

Ja, Sie haben absolut Recht, dass es nirgendwo eine Regel gibt, dass ein standardkonstruiertes Objekt in einem ähnlichen Zustand wie ein verschobenes Objekt sein sollte. Ich argumentiere jedoch, dass dies basierend auf dem Verhalten von eingebauten Typen eine natürliche Wahl sein könnte (zumindest im Fall von RAII). – kloffy

+0

Wenn ich jedoch sicher wäre, hätte ich die Frage nicht gestellt ...;) – kloffy

+0

@kloffy Es ist eigentlich eine sehr schlechte Wahl, ein verschobenes Objekt in einen default-initialisierten Zustand zu setzen, wie dies der Fall ist in der Regel weitaus weniger effizient als einfach den Zustand des bewegten und bewegten Objekts zu vertauschen. Und da der Staat gültig sein muss, aber nicht spezifiziert ist, gibt es keinen Grund, etwas weniger effizient zu machen. – Joe

1

Was Sie tun können, ist Lazy-Initialisierung: Haben Sie ein Flag (oder ein Null-Zeiger) in Ihrem Objekt, das angibt, ob das Objekt vollständig initialisiert ist. Dann haben Sie eine Member-Funktion, die dieses Flag verwendet, um die Initialisierung zu gewährleisten nach es ausgeführt wird. Ihr Standardkonstruktor muss lediglich das Initialisierungsflag auf "false" setzen. Wenn alle Mitglieder, die einen initialisierten Status benötigen, vor Beginn ihrer Arbeit ensure_initialization() aufrufen, haben Sie eine perfekte Semantik und keine doppelte schwere Initialisierung.

Beispiel:

class Foo { 
public: 
    Foo() : isInitialized(false) { }; 

    void ensureInitialization() { 
     if(isInitialized) return; 
     //the usual default constructor code 
     isInitialized = true; 
    }; 

    void bar() { 
     ensureInitialization(); 
     //the rest of the bar() implementation 
    }; 

private: 
    bool isInitialized; 
    //some heavy variables 
} 

Edit: den Overhead durch den Funktionsaufruf erzeugt zu reduzieren, können Sie etwas tun:

//In the .h file: 
class Foo { 
public: 
    Foo() : isInitialized(false) { }; 
    void bar(); 

private: 
    void initialize(); 

    bool isInitialized; 
    //some heavy variables 
} 

//In the .cpp file: 
#define ENSURE_INITIALIZATION() do { \ 
    if(!isInitialized) initialize(); \ 
} while(0) 

void Foo::bar() { 
    ENSURE_INITIALIZATION(); 
    //the rest of the bar() implementation 
} 

void Foo::initialize() { 
    //the usual default constructor code 
    isInitialized = true; 
} 

Dies stellt sicher, dass die Entscheidung zu initialisieren oder nicht wird inline, ohne die Initialisierung selbst zu inline. Das spätere würde die ausführbare Datei einfach aufblähen und die Instruktions-Cache-Effizienz verringern, aber die erste kann nicht automatisch ausgeführt werden, so dass Sie den Präprozessor dafür verwenden müssen.Der Overhead dieses Ansatzes sollte im Durchschnitt kleiner als ein Funktionsaufruf sein.

+0

Danke, dass Sie eine weitere interessante Alternative vorgeschlagen haben. Sicherlich eine Möglichkeit, sich vor Fehlern von Benutzern der Klasse zu schützen. Etwas mühsam, "secureInitialization" in alle Methoden zu setzen. – kloffy

+0

Das stimmt, und das ist der Grund, warum Sie über die Frage nachdenken müssen: Welcher Overhead ist größer? Unnötige Konstruktion oder ein Funktionsaufruf mit einer für jeden Methodenaufruf bewerteten Bedingung? Ein unnötiger Aufruf von new/delete zu vermeiden, bedeutet jedoch, mehr als 200 CPU-Zyklen zu vermeiden. Daher müssen Sie 'securityInitialization()' einige Male aufrufen, um eine gespeicherte Initialisierung auszugleichen. Aber wenn ich über den Overhead nachdenke, füge ich meiner Antwort eine kleine Optimierung hinzu :-) – cmaster