Als ich vor langer Zeit C ++ lernte, wurde mir stark betont, dass ein Teil des Punktes von C ++ darin besteht, dass genau wie Schleifen "Schleifeninvarianten" haben, Klassen auch Invarianten, die mit der Lebensdauer des Objekts verbunden sind - Dinge, die wahr sein sollten solange das Objekt lebt. Dinge, die von den Konstruktoren festgelegt und von den Methoden erhalten werden sollten. Die Kapselung / Zugriffskontrolle soll Ihnen dabei helfen, die Invarianten durchzusetzen. RAII ist eine Sache, die Sie mit dieser Idee machen können.
Seit C ++ 11 haben wir jetzt die Bewegungssemantik. Für eine Klasse, die das Bewegen unterstützt, endet das Bewegen von einem Objekt formell nicht seine Lebenszeit - die Bewegung soll es in einem "gültigen" Zustand belassen.
Ist es beim Entwerfen einer Klasse eine schlechte Praxis, wenn Sie sie so entwerfen, dass die Invarianten der Klasse nur bis zu dem Punkt erhalten bleiben, von dem aus sie verschoben werden? Oder ist das in Ordnung, wenn Sie es schneller machen können?
Nehmen wir an, ich habe einen nicht kopierbaren, aber verschiebbaren Ressourcentyp:
class opaque {
opaque(const opaque &) = delete;
public:
opaque(opaque &&);
...
void mysterious();
void mysterious(int);
void mysterious(std::vector<std::string>);
};
Und aus irgendeinem Grund muss ich einen kopierbaren Wrapper für dieses Objekt erstellen, damit es möglicherweise in einem vorhandenen Versandsystem verwendet werden kann.
class copyable_opaque {
std::shared_ptr<opaque> o_;
copyable_opaque() = delete;
public:
explicit copyable_opaque(opaque _o)
: o_(std::make_shared<opaque>(std::move(_o)))
{}
void operator()() { o_->mysterious(); }
void operator()(int i) { o_->mysterious(i); }
void operator()(std::vector<std::string> v) { o_->mysterious(v); }
};
In diesem copyable_opaque
Objekt besteht eine Invariante der bei der Konstruktion festgelegten Klasse darin, dass das Element o_
immer auf ein gültiges Objekt verweist, da es keinen Standard-ctor gibt und der einzige ctor, der kein Kopie-ctor ist, dies garantiert. Alle operator()
Methoden setzen voraus, dass diese Invariante gilt, und bewahren sie anschließend auf.
Wenn das Objekt jedoch verschoben o_
wird, zeigt es auf nichts. Und nach diesem Zeitpunkt führt der Aufruf einer der Methoden zu operator()
einem Absturz von UB /.
Wird das Objekt nie verschoben, bleibt die Invariante bis zum Aufruf von dtor erhalten.
Nehmen wir an, dass ich diese Klasse hypothetisch geschrieben habe und mein imaginärer Kollege Monate später UB erlebte, weil er in einer komplizierten Funktion, in der viele dieser Objekte aus irgendeinem Grund durcheinandergebracht wurden, von einem dieser Dinge wegging und später eines von beiden nannte seine Methoden. Klar ist es am Ende des Tages seine Schuld, aber ist diese Klasse "schlecht designt"?
Gedanken:
In C ++ ist es normalerweise eine schlechte Form, Zombie-Objekte zu erstellen, die explodieren, wenn Sie sie berühren.
Wenn Sie ein Objekt nicht konstruieren können, die Invarianten nicht ermitteln können, lösen Sie eine Ausnahme vom ctor aus. Wenn Sie die Invarianten in einer Methode nicht beibehalten können, melden Sie einen Fehler und führen Sie ein Rollback durch. Sollte dies für bewegte Objekte anders sein?Reicht es aus, im Header nur zu dokumentieren, dass es nach dem Verschieben dieses Objekts unzulässig ist, etwas anderes zu tun, als es zu zerstören?
Ist es besser, ständig zu behaupten, dass es in jedem Methodenaufruf gültig ist?
Wie so:
class copyable_opaque {
std::shared_ptr<opaque> o_;
copyable_opaque() = delete;
public:
explicit copyable_opaque(opaque _o)
: o_(std::make_shared<opaque>(std::move(_o)))
{}
void operator()() { assert(o_); o_->mysterious(); }
void operator()(int i) { assert(o_); o_->mysterious(i); }
void operator()(std::vector<std::string> v) { assert(o_); o_->mysterious(v); }
};
Die Behauptungen verbessern das Verhalten nicht wesentlich und verursachen eine Verlangsamung. Wenn Ihr Projekt das "Release Build / Debug Build" -Schema verwendet und nicht nur immer mit Assertions ausgeführt wird, ist dies meiner Meinung nach attraktiver, da Sie nicht für die Schecks im Release Build bezahlen. Wenn Sie keine Debug-Builds haben, scheint dies jedoch ziemlich unattraktiv zu sein.
- Ist es besser, die Klasse kopierbar, aber nicht beweglich zu machen?
Dies scheint ebenfalls schlecht zu sein und führt zu Leistungseinbußen, löst jedoch das "invariante" Problem auf einfache Weise.
Was halten Sie hier für die relevanten "Best Practices"?