Ich möchte eine Datei in einem Klassenkonstruktor öffnen. Es ist möglich, dass die Öffnung fehlschlägt und die Objektkonstruktion nicht abgeschlossen werden kann. Wie gehe ich mit diesem Fehler um? Ausnahme rauswerfen? Wenn dies möglich ist, wie geht man damit in einem Nicht-Throw-Konstruktor um?
75
Antworten:
Wenn eine Objektkonstruktion fehlschlägt, lösen Sie eine Ausnahme aus.
Die Alternative ist schrecklich. Sie müssten ein Flag erstellen, wenn die Konstruktion erfolgreich war, und es in jeder Methode überprüfen.
quelle
istream&
Parameters :)Ja.
Ihre Optionen sind:
if (X x) ...
(dh das Objekt kann in einem booleschen Kontext ausgewertet werden, normalerweise durch Bereitstellungoperator bool() const
oder ähnliche integrale Konvertierung), aber dann haben Sie keinenx
Spielraum dafür Fragen Sie nach Details des Fehlers. Dies kann zif (std::ifstream f(filename)) { ... } else ...;
bool worked; X x(&worked); if (worked) ...
if (X* p = x_factory()) ...
</li> <li>
X x; // nie verwendbar; if (init_x (& x)) ... `Kurz gesagt, C ++ bietet elegante Lösungen für diese Art von Problemen: in diesem Fall Ausnahmen. Wenn Sie sich künstlich daran hindern, sie zu verwenden, erwarten Sie nicht, dass es etwas anderes gibt, das halb so gute Arbeit leistet.
(PS Ich mag es, Variablen zu übergeben, die durch einen Zeiger geändert werden - wie
worked
oben beschrieben - ich weiß, dass die FAQ lite davon abhält, aber mit der Begründung nicht einverstanden ist. Ich bin nicht besonders an einer Diskussion darüber interessiert, es sei denn, Sie haben etwas, das nicht in den FAQ behandelt wird.)quelle
Der neue C ++ - Standard definiert dies auf so viele Arten neu, dass es Zeit ist, diese Frage erneut zu prüfen.
Beste Wahl:
Optional benannt : Haben Sie einen minimalen privaten Konstruktor und einen benannten Konstruktor :
static std::experimental::optional<T> construct(...)
. Letzterer versucht, Mitgliedsfelder einzurichten, stellt eine Invariante sicher und ruft den privaten Konstruktor nur auf, wenn dies mit Sicherheit erfolgreich ist. Der private Konstruktor füllt nur Mitgliedsfelder. Es ist einfach, das optionale zu testen und es ist kostengünstig (selbst die Kopie kann in einer guten Implementierung geschont werden).Funktionsstil : Die gute Nachricht ist, dass (nicht benannte) Konstruktoren niemals virtuell sind. Daher können Sie sie durch eine statische Vorlagenelementfunktion ersetzen, die neben den Konstruktorparametern zwei (oder mehr) Lambdas benötigt: eines, wenn es erfolgreich war, eines, wenn es fehlgeschlagen ist. Der 'echte' Konstruktor ist immer noch privat und kann nicht fehlschlagen. Das klingt vielleicht übertrieben, aber Lambdas werden von Compilern wunderbar optimiert. Auf
if
diese Weise können Sie sogar das Optionale schonen .Gute Wahl:
Ausnahme : Wenn alles andere fehlschlägt, verwenden Sie eine Ausnahme. Beachten Sie jedoch, dass Sie während der statischen Initialisierung keine Ausnahme abfangen können. Eine mögliche Problemumgehung besteht darin, dass der Rückgabewert einer Funktion das Objekt in diesem Fall initialisiert.
Builder-Klasse : Wenn die Konstruktion kompliziert ist, verfügen Sie über eine Klasse, die die Validierung und möglicherweise eine Vorverarbeitung so weit durchführt, dass die Operation nicht fehlschlagen kann. Lassen Sie es eine Möglichkeit haben, den Status zurückzugeben (yep, Fehlerfunktion). Ich persönlich würde es nur stapelbar machen, damit die Leute es nicht weitergeben. Lassen Sie es dann eine
.build()
Methode haben, die die andere Klasse konstruiert. Wenn der Builder ein Freund ist, kann der Konstruktor privat sein. Möglicherweise ist sogar etwas erforderlich, das nur der Builder erstellen kann, damit dokumentiert wird, dass dieser Konstruktor nur vom Builder aufgerufen werden darf.Schlechte Entscheidungen: (aber oft gesehen)
Flag : Verwirren Sie Ihre Klasseninvariante nicht, indem Sie einen "ungültigen" Status haben. Genau deshalb haben wir
optional<>
. Denken Sie daranoptional<T>
, das kann ungültig sein,T
das kann nicht. Eine (Mitglieds- oder globale) Funktion, die nur für gültige Objekte funktioniert, funktioniert fürT
. Eine, die sicherlich gültige Werke zurückgibtT
. Eine, die möglicherweise eine ungültige Objektrückgabe zurückgibtoptional<T>
. Eine, die ein Objekt ungültig machen könnte, ist non-constoptional<T>&
oderoptional<T>*
. Auf diese Weise müssen Sie nicht jede Funktion einchecken, für die Ihr Objekt gültig ist (und dieseif
können etwas teuer werden), aber dann scheitern Sie auch nicht am Konstruktor.Standardkonstrukt und Setter : Dies ist im Grunde dasselbe wie Flag, nur dass Sie diesmal gezwungen sind, ein veränderliches Muster zu haben. Vergessen Sie Setter, sie erschweren Ihre Klasseninvariante unnötig. Denken Sie daran, Ihre Klasse einfach zu halten, nicht einfach aufzubauen.
Standardkonstrukt und
init()
dasoptional<>
erfordert einen ctor-Parameter : Dies ist nichts Besseres als eine Funktion, die ein zurückgibt , aber zwei Konstruktionen erfordert und Ihre Invariante durcheinander bringt .Nehmen
bool& succeed
wir: Das haben wir vorher gemachtoptional<>
. Der Grundoptional<>
ist überlegen, Sie können diesucceed
Flagge nicht fälschlicherweise (oder nachlässig!) Ignorieren und das teilweise konstruierte Objekt weiter verwenden.Factory, die einen Zeiger zurückgibt : Dies ist weniger allgemein, da das Objekt dynamisch zugewiesen werden muss. Entweder geben Sie einen bestimmten Typ eines verwalteten Zeigers zurück (und beschränken daher das Zuordnungs- / Bereichsschema) oder Sie geben nackte ptr zurück und riskieren, dass Clients auslaufen. In Bezug auf die Leistung von Verschiebungsschemata ist dies möglicherweise weniger wünschenswert (Einheimische sind, wenn sie auf dem Stapel gehalten werden, sehr schnell und cachefreundlich).
Beispiel:
#include <iostream> #include <experimental/optional> #include <cmath> class C { public: friend std::ostream& operator<<(std::ostream& os, const C& c) { return os << c.m_d << " " << c.m_sqrtd; } static std::experimental::optional<C> construct(const double d) { if (d>=0) return C(d, sqrt(d)); return std::experimental::nullopt; } template<typename Success, typename Failed> static auto if_construct(const double d, Success success, Failed failed = []{}) { return d>=0? success( C(d, sqrt(d)) ): failed(); } /*C(const double d) : m_d(d), m_sqrtd(d>=0? sqrt(d): throw std::logic_error("C: Negative d")) { }*/ private: C(const double d, const double sqrtd) : m_d(d), m_sqrtd(sqrtd) { } double m_d; double m_sqrtd; }; int main() { const double d = 2.0; // -1.0 // method 1. Named optional if (auto&& COpt = C::construct(d)) { C& c = *COpt; std::cout << c << std::endl; } else { std::cout << "Error in 1." << std::endl; } // method 2. Functional style C::if_construct(d, [&](C c) { std::cout << c << std::endl; }, [] { std::cout << "Error in 2." << std::endl; }); }
quelle
bool& succeed
muss eigentlich kein bool sein. Es könnte sich auch um einen Fehlercode handeln, der Ihnen mehr Informationen als ein std :: optional gibt.std::variant<ValueType, ErrorInfo>
- keine veränderbare Eingabe erforderlich. Gehen Sie für,boost::variant
wenn Sie es noch nicht in Ihrem Compiler haben.Mein Vorschlag für diese spezielle Situation lautet: Wenn Sie nicht möchten, dass ein Konstruktor ausfällt, weil Sie eine Datei nicht öffnen können, vermeiden Sie diese Situation. Übergeben Sie eine bereits geöffnete Datei an den Konstruktor, wenn Sie dies wünschen, dann kann es nicht scheitern ...
quelle
Ich möchte eine Datei in einem Klassenkonstruktor öffnen.
Mit ziemlicher Sicherheit eine schlechte Idee. Sehr wenige Fälle beim Öffnen einer Datei während der Erstellung sind angemessen.
Es ist möglich, dass die Öffnung fehlschlägt und die Objektkonstruktion nicht abgeschlossen werden kann. Wie gehe ich mit diesem Fehler um? Ausnahme rauswerfen?
Ja, das wäre der Weg.
Wenn dies möglich ist, wie geht man damit in einem Nicht-Throw-Konstruktor um?
Machen Sie es möglich, dass ein vollständig erstelltes Objekt Ihrer Klasse ungültig sein kann. Dies bedeutet, Validierungsroutinen bereitzustellen, diese zu verwenden usw. ick
quelle
istream
oderostream
Objekt. Auf diese Weise können Sie testen, ob der Stream durch einen Stringstream ersetzt wird.open
in Ihrer Klasse verhindern Sie beispielsweise eine Wiederverwendung von beispielsweise einer Speicherzuordnungsdatei. Auf der anderen Seite können Sie mithilfe einer Basisklasse (stream-like) alles übergeben, was die Schnittstelle implementiert. Dies erleichtert das Testen und die Wiederverwendung.std::fstream
öffnet die Datei in ihrem Konstruktor.Eine Möglichkeit besteht darin, eine Ausnahme auszulösen. Eine andere Möglichkeit ist die Funktion 'bool is_open ()' oder 'bool is_valid ()', die false zurückgibt, wenn im Konstruktor ein Fehler aufgetreten ist.
Einige Kommentare hier sagen, dass es falsch ist, eine Datei im Konstruktor zu öffnen. Ich werde darauf hinweisen, dass ifstream, das Teil des C ++ - Standards ist, den folgenden Konstruktor hat:
explicit ifstream ( const char * filename, ios_base::openmode mode = ios_base::in );
Es wird keine Ausnahme ausgelöst, aber es hat eine is_open-Funktion:
bool is_open ( );
quelle
ifstream
ist ein RAII-Objekt, das den Verweis auf die Datei verwaltet. Das ist ein ganz anderer Fall als die meisten "Klassen, die Dateien in ihren Konstruktoren öffnen" (die ich sowieso gesehen habe). Guter C ++ - Code hat auch keine explizitendelete
Anweisungen, aber es ist unmöglich, intelligente Zeiger ohne sie zu implementieren.Ein Konstruktor kann eine Datei öffnen (nicht unbedingt eine schlechte Idee) und möglicherweise auslösen, wenn das Öffnen der Datei fehlschlägt oder wenn die Eingabedatei keine kompatiblen Daten enthält.
Es ist ein vernünftiges Verhalten eines Konstruktors, eine Ausnahme auszulösen. Sie sind jedoch hinsichtlich ihrer Verwendung eingeschränkt.
Sie können keine statischen Instanzen (auf Dateiebene der Kompilierungseinheit) dieser Klasse erstellen, die vor "main ()" erstellt wurden, da ein Konstruktor immer nur in den regulären Ablauf geworfen werden sollte.
Dies kann sich auf eine spätere "erstmalige" verzögerte Auswertung erstrecken, bei der etwas geladen wird, wenn es zum ersten Mal benötigt wird, beispielsweise in einem Boost :: Once-Konstrukt, das die Funktion call_once niemals auslösen sollte.
Sie können es in einer IOC-Umgebung (Inversion of Control / Dependency Injection) verwenden. Aus diesem Grund sind IOC-Umgebungen vorteilhaft.
Stellen Sie sicher, dass Ihr Destruktor nicht aufgerufen wird, wenn Ihr Konstruktor wirft. Alles, was Sie vor diesem Punkt im Konstruktor initialisiert haben, muss in einem RAII-Objekt enthalten sein.
Gefährlicher kann es übrigens sein, die Datei im Destruktor zu schließen, wenn dadurch der Schreibpuffer geleert wird. Es gibt überhaupt keine Möglichkeit, Fehler, die an diesem Punkt auftreten können, richtig zu behandeln.
Sie können dies ausnahmslos behandeln, indem Sie das Objekt in einem "fehlgeschlagenen" Zustand belassen. Dies ist die Art und Weise, wie Sie dies tun müssen, wenn das Werfen nicht erlaubt ist, aber Ihr Code muss natürlich nach dem Fehler suchen.
quelle