Wie gehe ich mit Fehlern im Konstruktor in C ++ um?

75

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?

Thomson
quelle
"Wenn Sie einen wiederherstellbaren Fehlerbehandlungsmechanismus nicht ohne Ausnahmen in einem Konstruktor verwenden können, verwenden Sie keinen Konstruktor." Lesen Sie hier mehr: foonathan.net/2017/01/exceptions-constructor .
Gabriel Staples

Antworten:

38

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.

BЈовић
quelle
5
Es wäre großartig, tatsächlich auch eine "echte" Lösung vorzuschlagen, beispielsweise die Übergabe eines istream&Parameters :)
Matthieu M.
@Matthie Ja, die Umkehrung der Kontrolle. Ich vergesse es immer wieder. Danke für die Erinnerung.
BЈовић
@Nils Das würde man bei jeder normalen Codeüberprüfung als Greuel bezeichnen. Außerdem müssen Sie überprüfen, ob sich das Objekt in einem normalen Zustand befindet, bevor Sie etwas damit tun.
B 20овић
28

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?

Ja.

Wenn dies möglich ist, wie geht man damit in einem Nicht-Throw-Konstruktor um?

Ihre Optionen sind:

  • Neugestaltung der App, sodass keine Konstruktoren benötigt werden, um nicht zu werfen - tun Sie es wirklich, wenn möglich
  • Fügen Sie eine Flagge hinzu und testen Sie den erfolgreichen Bau
    • Sie könnten jede Mitgliedsfunktion, die legitimerweise unmittelbar nach dem Konstruktortest aufgerufen wird, das Flag testen lassen, idealerweise auslösen, wenn es gesetzt ist, aber ansonsten einen Fehlercode zurückgeben
      • Dies ist hässlich und schwer zu korrigieren, wenn eine flüchtige Gruppe von Entwicklern am Code arbeitet.
      • Sie können dies zur Kompilierungszeit überprüfen, indem Sie das Objekt polymorph auf eine von zwei Implementierungen verschieben lassen: eine erfolgreich erstellte und eine immer fehlerhafte Version, die jedoch Heap-Nutzung und Leistungskosten verursacht.
    • Sie können die Last der Überprüfung des Flags vom aufgerufenen Code auf den Angerufenen verlagern, indem Sie eine Anforderung dokumentieren, dass sie eine "is_valid ()" - oder ähnliche Funktion aufrufen, bevor Sie das Objekt verwenden: wieder fehleranfällig und hässlich, aber noch verteilter, nicht durchsetzbar und außer Kontrolle.
      • Sie können dies für den Aufrufer ein wenig einfacher und lokaler gestalten, wenn Sie Folgendes unterstützen: if (X x) ...(dh das Objekt kann in einem booleschen Kontext ausgewertet werden, normalerweise durch Bereitstellung operator bool() constoder ähnliche integrale Konvertierung), aber dann haben Sie keinen xSpielraum dafür Fragen Sie nach Details des Fehlers. Dies kann zif (std::ifstream f(filename)) { ... } else ...;
  • Lassen Sie den Anrufer einen Stream bereitstellen, für dessen Öffnen er verantwortlich ist ... (bekannt als Dependency Injection oder DI) ... in einigen Fällen funktioniert dies nicht so gut:
    • Sie können immer noch Fehler haben, wenn Sie den Stream in Ihrem Konstruktor verwenden. Was dann?
    • Die Datei selbst kann ein Implementierungsdetail sein, das für Ihre Klasse privat sein sollte, anstatt für den Aufrufer verfügbar zu sein. Was ist, wenn Sie diese Anforderung später entfernen möchten? Beispiel: Sie haben möglicherweise eine Nachschlagetabelle mit vorberechneten Ergebnissen aus einer Datei gelesen, Ihre Berechnungen jedoch so schnell durchgeführt, dass keine Vorberechnung erforderlich ist. Es ist schmerzhaft (in einer Unternehmensumgebung manchmal sogar unpraktisch), die Datei zu jedem Zeitpunkt zu entfernen Client-Nutzung und erzwingt viel mehr Neukompilierung als potenziell einfaches erneutes Verknüpfen.
  • Erzwingen Sie, dass der Aufrufer einen Puffer für eine Erfolgs- / Fehler- / Fehlerbedingungsvariable bereitstellt, die der Konstruktor festlegt: z bool worked; X x(&worked); if (worked) ...
    • Diese Belastung und Ausführlichkeit zieht die Aufmerksamkeit auf sich und macht den Aufrufer hoffentlich viel bewusster für die Notwendigkeit, die Variable nach der Konstruktion des Objekts zu konsultieren
  • Erzwingen Sie, dass der Aufrufer das Objekt über eine andere Funktion erstellt, die Rückkehrcodes und / oder Ausnahmen verwenden kann:
    • if (X* p = x_factory()) ...
    • Smart_Ptr_Throws_On_Null_Deref p_x = x_factory (); </li> <li>X x; // nie verwendbar; if (init_x (& x)) ... `
    • etc...

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 workedoben 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.)

Tony Delroy
quelle
16

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 ifdiese 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 daran optional<T>, das kann ungültig sein, Tdas kann nicht. Eine (Mitglieds- oder globale) Funktion, die nur für gültige Objekte funktioniert, funktioniert für T. Eine, die sicherlich gültige Werke zurückgibt T. Eine, die möglicherweise eine ungültige Objektrückgabe zurückgibt optional<T>. Eine, die ein Objekt ungültig machen könnte, ist non-const optional<T>&oder optional<T>*. Auf diese Weise müssen Sie nicht jede Funktion einchecken, für die Ihr Objekt gültig ist (und diese ifkö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 .

  • Nehmenbool& succeed wir: Das haben wir vorher gemacht optional<>. Der Grund optional<>ist überlegen, Sie können die succeedFlagge 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;
    });
}
lorro
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.
Ganea Dan Andrei
@GaneaDanAndrei: Wenn Ihr Typ entweder den Wert oder die Fehlerinformationen enthält, geben Sie ihn zurück (nicht als Argument). Wenn es nur die Fehlerinformationen enthält, geben Sie möglicherweise eine std::variant<ValueType, ErrorInfo>- keine veränderbare Eingabe erforderlich. Gehen Sie für, boost::variantwenn Sie es noch nicht in Ihrem Compiler haben.
Lorro
15

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 ...

jcoder
quelle
3
Und was ist, wenn der Inhalt der Datei leer ist? Oder enthält ungültige Daten?
CashCow
5
Auf dem Poster stand nur, dass er die Datei im Konstruktor öffnen wollte. Wenn er mehr tut, könnte dies natürlich auf andere Weise scheitern und müsste angemessen behandelt werden.
Jcoder
4

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

Edward Strange
quelle
6
Warum ist das Öffnen einer Datei oder einer anderen Ressource in einem Konstruktor eine schlechte Idee?
Jörgen Sigvardsson
3
@ Jörgen Sigvardsson: Weil es besser wäre , die Klasse in Bezug auf einem bestimmten zu schreiben istreamoder ostreamObjekt. Auf diese Weise können Sie testen, ob der Stream durch einen Stringstream ersetzt wird.
Billy ONeal
1
@Jorgen Sigvardsson: Dies widerspricht der Philosophie der Abhängigkeitsinjektion. Ihre Frage ist besser umgekehrt zu lesen: Warum binden Sie Ihre Hand in den Rücken? Durch die explizite Verwendung von a openin 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.
Matthieu M.
1
@ Crazy: Also macht die Standardbibliothek es auch falsch? std::fstreamöffnet die Datei in ihrem Konstruktor.
Jalf
1
@Matthieu: Ich sehe nicht, wie viel Arbeit in Konstruktoren mit DI zu tun hat ...? Das Vorhandensein eines "hart arbeitenden" Konstruktors schließt auch die Bereitstellung einer offenen Methode nicht aus.
Jörgen Sigvardsson
4

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 ( );
Sashoalm
quelle
1
Nun, ifstreamist 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 expliziten deleteAnweisungen, aber es ist unmöglich, intelligente Zeiger ohne sie zu implementieren.
Billy ONeal
Um es nur zu erwähnen: Viele Leute mögen den Ansatz is_open () oder is_valid () nicht und halten ihn für schlecht. Dies liegt daran, dass Benutzer der Klasse leicht vergessen können, diese Methode aufzurufen, und Sie am Ende eine teilweise konstruierte Klasse haben und den is_open () -Test in viele Mitgliedsfunktionen aufnehmen müssen. In einigen Fällen kann dies jedoch eine Option sein.
Sstn
4

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.

Goldesel
quelle