it-swarm.com.de

Wie gehe ich mit Fehlerfällen im C ++ - Klassenkonstruktor um?

Ich habe eine CPP-Klasse, deren Konstruktor einige Operationen ausführt. Einige dieser Vorgänge können fehlschlagen. Ich weiß, dass Konstruktoren nichts zurückgeben.

Meine Fragen sind:

  1. Dürfen andere Operationen als das Initialisieren von Mitgliedern in einem Konstruktor ausgeführt werden?

  2. Ist es möglich, der aufrufenden Funktion mitzuteilen, dass einige Operationen im Konstruktor fehlgeschlagen sind?

  3. Kann ich new ClassName() NULL zurückgeben lassen, wenn im Konstruktor einige Fehler auftreten?

21
MayurK
  1. Ja, obwohl einige Codierungsstandards dies möglicherweise verbieten.

  2. Ja. Der empfohlene Weg ist, eine Ausnahme auszulösen. Alternativ können Sie die Fehlerinformationen im Objekt speichern und Methoden für den Zugriff auf diese Informationen bereitstellen.

  3. Nein.

42
Sebastian Redl

Sie können eine statische Methode erstellen, die die Berechnung durchführt und im Erfolgsfall oder im Fehlerfall entweder ein Objekt zurückgibt.

Abhängig davon, wie diese Konstruktion des Objekts ausgeführt wird, ist es möglicherweise besser, ein anderes Objekt zu erstellen, das die Konstruktion von Objekten in einer nicht statischen Methode ermöglicht.

Das indirekte Aufrufen eines Konstruktors wird häufig als "Fabrik" bezeichnet.

Auf diese Weise können Sie auch ein Null-Objekt zurückgeben. Dies ist möglicherweise eine bessere Lösung als die Rückgabe von Null.

20
null

@SebastianRedl gab bereits die einfachen, direkten Antworten, aber einige zusätzliche Erklärungen könnten nützlich sein.

TL; DR = Es gibt eine Stilregel, um Konstruktoren einfach zu halten. Es gibt Gründe dafür, aber diese Gründe beziehen sich hauptsächlich auf einen historischen (oder einfach schlechten) Codierungsstil. Die Behandlung von Ausnahmen in Konstruktoren ist gut definiert, und Destruktoren werden weiterhin für vollständig konstruierte lokale Variablen und Mitglieder aufgerufen, was bedeutet, dass es keine Probleme mit idiomatischem C++ - Code geben sollte. Die Stilregel bleibt trotzdem bestehen, aber normalerweise ist das kein Problem - nicht alle Initialisierungen müssen im Konstruktor sein, und insbesondere nicht unbedingt in diesem Konstruktor.


Es ist eine übliche Stilregel, dass Konstruktoren das absolute Minimum tun sollten, um einen definierten gültigen Status einzurichten. Wenn Ihre Initialisierung komplexer ist, sollte sie außerhalb des Konstruktors behandelt werden. Wenn Ihr Konstruktor keinen kostengünstigen Initialisierungswert einrichten kann, sollten Sie die von Ihrer Klasse erzwungenen Invaranten schwächen, um einen hinzuzufügen. Wenn beispielsweise das Zuweisen von Speicher für die Verwaltung Ihrer Klasse zu teuer ist, fügen Sie einen noch nicht zugewiesenen Nullstatus hinzu, da Sonderfallzustände wie Null natürlich niemandem Probleme bereiteten. Hm.

Obwohl üblich, ist es in dieser extremen Form sicherlich alles andere als absolut. Insbesondere, wie mein Sarkasmus zeigt, bin ich im Lager, das sagt, dass das Schwächen von Invarianten fast immer ein zu hoher Preis ist. Es gibt jedoch Gründe für die Stilregel, und es gibt Möglichkeiten, sowohl minimale Konstruktoren als auch starke Invarianten zu haben.

Die Gründe beziehen sich auf die automatische Bereinigung des Destruktors, insbesondere angesichts von Ausnahmen. Grundsätzlich muss es einen genau definierten Punkt geben, an dem der Compiler für das Aufrufen von Destruktoren verantwortlich wird. Während Sie sich noch in einem Konstruktoraufruf befinden, ist das Objekt nicht unbedingt vollständig konstruiert, daher ist es nicht gültig, den Destruktor für dieses Objekt aufzurufen. Daher wird die Verantwortung für die Zerstörung des Objekts erst nach erfolgreichem Abschluss des Konstruktors auf den Compiler übertragen. Dies ist als RAII (Resource Allocation Is Initialization) bekannt, was nicht wirklich der beste Name ist.

Wenn im Konstruktor ein Ausnahmefall auftritt, muss alles, was teilweise erstellt wurde, explizit bereinigt werden, normalerweise in einem try .. catch.

Für Komponenten des Objekts, die bereits erfolgreich erstellt wurden, liegt jedoch bereits die Verantwortung des Compilers. Dies bedeutet, dass es in der Praxis keine große Sache ist. z.B.

classname (args) : base1 (args), member2 (args), member3 (args)
{
}

Der Körper dieses Konstruktors ist leer. Solange die Konstruktoren für base1, member2 Und member3 Ausnahmesicher sind, besteht kein Grund zur Sorge. Wenn beispielsweise der Konstruktor von member2 Wirft, ist dieser Konstruktor für die Bereinigung selbst verantwortlich. Die Basis base1 Wurde bereits vollständig erstellt, sodass ihr Destruktor automatisch aufgerufen wird. member3 Wurde noch nie teilweise erstellt und muss daher nicht bereinigt werden.

Selbst wenn es einen Body gibt, werden lokale Variablen, die vor dem Auslösen der Ausnahme vollständig erstellt wurden, wie jede andere Funktion automatisch zerstört. Konstruktorkörper, die mit rohen Zeigern jonglieren oder einen impliziten Status "besitzen" (an anderer Stelle gespeichert) - was normalerweise bedeutet, dass ein Funktionsaufruf zum Starten/Erfassen mit einem Aufruf zum Beenden/Freigeben abgeglichen werden muss - können Ausnahmesicherheitsprobleme verursachen, aber das eigentliche Problem dort kann eine Ressource über eine Klasse nicht ordnungsgemäß verwalten. Wenn Sie beispielsweise im Konstruktor Rohzeiger durch unique_ptr Ersetzen, wird der Destruktor für unique_ptr Bei Bedarf automatisch aufgerufen.

Es gibt noch andere Gründe, warum Leute Do-the-Minimum-Konstruktoren bevorzugen. Eine ist einfach, weil die Stilregel existiert, viele Leute nehmen an, dass Konstruktoraufrufe billig sind. Eine Möglichkeit, dies zu erreichen und dennoch starke Invarianten zu haben, besteht darin, eine separate Factory/Builder-Klasse zu haben, die stattdessen die geschwächten Invarianten enthält und den erforderlichen Anfangswert mithilfe (möglicherweise vieler) normaler Member-Funktionsaufrufe festlegt. Wenn Sie den gewünschten Ausgangszustand haben, übergeben Sie dieses Objekt als Argument an den Konstruktor für die Klasse mit den starken Invarianten. Das kann den Mut des Objekts mit schwachen Invarianten "stehlen" - Bewegungssemantik - was eine billige (und normalerweise noexcept) Operation ist.

Und natürlich können Sie das in eine make_whatever () - Funktion einschließen, sodass Aufrufer dieser Funktion die Klasseninstanz mit geschwächten Invarianten nie sehen müssen.

5
Steve314