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

21

Ich habe eine CPP-Klasse, deren Konstruktor einige Operationen ausführt. Einige dieser Vorgänge schlagen möglicherweise fehl. Ich weiß, dass Konstrukteure 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, wenn im Konstruktor Fehler auftreten?

MayurK
quelle
22
Sie können eine Ausnahme aus dem Konstruktor heraus auslösen. Es ist ein völlig gültiges Muster.
Andy
1
Sie sollten sich wahrscheinlich einige der Schöpfungsmuster des GoF ansehen . Ich empfehle das Fabrikmuster.
SpaceTrucker
2
Ein häufiges Beispiel für Nummer 1 ist die Datenvalidierung. Wenn Sie eine Klasse Squaremit einem Konstruktor haben, der einen Parameter, die Länge einer Seite, akzeptiert, möchten Sie überprüfen, ob dieser Wert größer als 0 ist.
David sagt Reinstate Monica
1
Lassen Sie mich bei der ersten Frage warnen, dass sich virtuelle Funktionen in Konstruktoren nicht intuitiv verhalten können. Gleiches gilt für Dekonstruktoren. Vorsicht bei solchen Anrufen.
1
# 3 - Warum solltest du NULL zurückgeben? Einer der Vorteile von OO besteht darin, KEINE Rückgabewerte überprüfen zu müssen. Fangen Sie einfach () die entsprechenden möglichen Ausnahmen ab.
MrWonderful

Antworten:

42
  1. Ja, obwohl einige Codierungsstandards dies möglicherweise untersagen.

  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.

Sebastian Redl
quelle
4
Sofern sich das Objekt nicht noch im gültigen Zustand befindet, obwohl ein Teil der Konstruktorargumente nicht den Anforderungen entsprach und daher als Fehler markiert ist, wird 2) wirklich nicht empfohlen, dies zu tun. Es ist besser, wenn ein Objekt entweder in einem gültigen Zustand existiert oder überhaupt nicht existiert.
Andy
@DavidPacker Einverstanden, siehe hier: stackoverflow.com/questions/77639/… Aber einige Codierungsrichtlinien verbieten Ausnahmen, was für Konstruktoren problematisch ist.
Sebastian Redl
Irgendwie habe ich dir für diese Antwort schon eine Gegenstimme gegeben, Sebastian. Interessant. : D
Andy
10
@ooxi Nein, das ist es nicht. Ihr überschriebenes new wird aufgerufen, um Speicher zuzuweisen, aber der Aufruf des Konstruktors erfolgt durch den Compiler, nachdem Ihr Operator zurückgekehrt ist, was bedeutet, dass Sie den Fehler nicht abfangen können. Dies setzt voraus, dass new überhaupt aufgerufen wird. Dies gilt nicht für stapelzugeordnete Objekte, bei denen es sich überwiegend um Objekte handeln sollte.
Sebastian Redl
1
Für # 1 ist RAII ein gängiges Beispiel, bei dem möglicherweise mehr im Konstruktor getan werden muss.
Eric
20

Sie können eine statische Methode erstellen, die die Berechnung ausführt und bei Erfolg oder Nichterfolg 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.

Indirektes Aufrufen eines Konstruktors wird häufig als "Factory" bezeichnet.

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

Null
quelle
Vielen Dank, dass Sie @null! Leider kann ich hier keine zwei Antworten akzeptieren :( Sonst hätte ich diese Antwort auch akzeptiert !!
Nochmals vielen
@ MayurK keine Sorge, die akzeptierte Antwort ist nicht die richtige Antwort zu markieren , sondern die, die für Sie funktioniert hat.
null
3
@null: In C ++ können Sie nicht einfach zurückkehren NULL. In int foo() { return NULLwürden Sie beispielsweise tatsächlich 0(Null) ein Ganzzahlobjekt zurückgeben. In würden std::string foo() { return NULL; }Sie versehentlich aufrufen, std::string::string((const char*)NULL)was Undefiniertes Verhalten ist (NULL zeigt nicht auf eine mit \ 0 abgeschlossene Zeichenfolge).
MSalters
3
std :: optional mag ein Ausweg sein, aber Sie könnten immer boost :: optional verwenden, wenn Sie diesen Weg einschlagen möchten.
Sean Burton
1
@Vld: In C ++ sind Objekte nicht auf Klassentypen beschränkt. Und mit der generischen Programmierung ist es nicht ungewöhnlich, dass Fabriken für entstehen int. ZB std::allocator<int>ist eine vollkommen gesunde Fabrik.
MSalters
5

@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 meist 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 Member aufgerufen, was bedeutet, dass es im idiomatischen C ++ - Code keine Probleme geben sollte. Die Stilregel bleibt trotzdem bestehen, aber normalerweise ist dies kein Problem - nicht alle Initialisierungen müssen im Konstruktor und insbesondere nicht unbedingt im Konstruktor erfolgen.


Es ist eine gängige 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 abschwächen, um einen hinzuzufügen. Wenn das Zuweisen von Speicherplatz für die Verwaltung Ihrer Klasse beispielsweise zu teuer ist, fügen Sie einen noch nicht zugewiesenen Nullstatus hinzu, da natürlich Sonderfälle wie Null nie zu Problemen geführt haben. Hm.

Obwohl üblich, sicherlich in dieser extremen Form ist es sehr weit vom Absoluten entfernt. Insbesondere bin ich, wie mein Sarkasmus zeigt, im Lager, das besagt, dass es fast immer zu teuer ist, Invarianten zu schwächen. 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 dafür liegen in der automatischen Destruktor-Bereinigung, insbesondere angesichts von Ausnahmen. Grundsätzlich muss es einen genau definierten Punkt geben, an dem der Compiler für den Aufruf von Destruktoren verantwortlich ist. Während Sie sich noch in einem Konstruktoraufruf befinden, ist das Objekt nicht unbedingt vollständig konstruiert, sodass es nicht gültig ist, den Destruktor für dieses Objekt aufzurufen. Daher wird die Verantwortung für die Zerstörung des Objekts erst dann auf den Compiler übertragen, wenn der Konstruktor erfolgreich abgeschlossen wurde. Dies ist als RAII (Resource Allocation Is Initialization) bekannt, was nicht wirklich der beste Name ist.

Wenn ein Ausnahmefehler im Konstruktor auftritt, muss alles, was als Teil erstellt wurde, explizit bereinigt werden, normalerweise in a try .. catch.

Allerdings Komponenten des Objekts , das bereits erfolgreich aufgebaut sind bereits die Compiler Verantwortung. Dies bedeutet, dass es in der Praxis keine große Sache ist. z.B

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

Der Body dieses Konstruktors ist leer. Solange die Konstrukteure für base1, member2und member3Ausnahme sicher sind, gibt es nichts zu befürchten. Wenn zum Beispiel der Konstruktor von member2wirft, ist dieser Konstruktor dafür verantwortlich, sich selbst zu bereinigen. Die Basis base1wurde bereits vollständig erstellt, sodass der Destruktor automatisch aufgerufen wird. member3wurde noch nie teilweise gebaut, muss also nicht aufgeräumt werden.

Auch wenn es einen Body gibt, werden lokale Variablen, die vor dem Auslösen der Exception vollständig erstellt wurden, wie jede andere Funktion automatisch zerstört. Konstruktorkörper, die mit unformatierten Zeigern jonglieren oder einen impliziten Zustand "besitzen" (der an einer anderen Stelle gespeichert ist) - was normalerweise bedeutet, dass ein Funktionsaufruf begin / acquis mit einem Aufruf end / release abgeglichen werden muss - können Ausnahmesicherheitsprobleme verursachen, aber das eigentliche Problem dort kann eine Ressource nicht ordnungsgemäß über eine Klasse verwalten. Wenn Sie beispielsweise unique_ptrim Konstruktor rohe Zeiger durch ersetzen , wird der Destruktor für unique_ptrbei Bedarf automatisch aufgerufen.

Es gibt noch andere Gründe, warum die Leute Do-the-Minimum-Konstruktoren bevorzugen. Zum einen gehen viele Leute davon aus, dass Konstruktoraufrufe billig sind, weil die Stilregel existiert. Eine Möglichkeit, dies zu erreichen und dennoch starke Invarianten zu haben, besteht darin, eine separate Factory- / Builder-Klasse zu haben, die stattdessen die abgeschwächten Invarianten enthält und die den erforderlichen Anfangswert mithilfe (möglicherweise vieler) normaler Memberfunktionsaufrufe einrichtet. Wenn Sie den erforderlichen Anfangszustand erreicht 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" - die Semantik verschieben - 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 niemals die Klasseninstanz "Geschwächte Invarianten" sehen müssen.

Steve314
quelle
Der Absatz, in dem Sie schreiben: "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 Destruktion des Objekts nur auf den Compiler übertragen "Wenn der Konstruktor erfolgreich abgeschlossen wurde" könnte ein Update zum Delegieren von Konstruktoren wirklich hilfreich sein. Das Objekt ist vollständig konstruiert, wenn ein am häufigsten abgeleiteter Konstruktor fertig ist, und der Destruktor wird aufgerufen, wenn eine Ausnahme innerhalb eines delegierenden Konstruktors auftritt.
Ben Voigt
Daher kann der Konstruktor "do-the-minimum" privat sein, und die Funktion "make_whatever ()" kann ein anderer Konstruktor sein, der den privaten Konstruktor aufruft.
Ben Voigt
Dies ist nicht die Definition von RAII, mit der ich vertraut bin. Mein Verständnis von RAII besteht darin, absichtlich eine Ressource im (und nur im) Konstruktor eines Objekts zu erfassen und in seinem Destruktor freizugeben. Auf diese Weise kann das Objekt auf dem Stapel verwendet werden, um die Erfassung und Freigabe der von ihm eingekapselten Ressourcen automatisch zu verwalten. Das klassische Beispiel ist ein Schloss, das beim Bau einen Mutex erhält und ihn bei Zerstörung freigibt.
Eric
1
@Eric - Ja, es ist absolut üblich - eine übliche Praxis, die allgemein als RAII bezeichnet wird. Es bin nicht nur ich, der die Definition ausdehnt - es ist sogar Stroustrup, in einigen Gesprächen. Ja, bei RAII geht es um das Verknüpfen von Ressourcenlebenszyklen mit Objektlebenszyklen, wobei das mentale Modell das Eigentum ist.
Steve314
1
@Eric - Vorherige Antworten wurden gelöscht, weil sie schlecht erklärt wurden. Auf jeden Fall sind Objekte selbst Ressourcen, deren Eigentümer sie sein können. Alles sollte einen Eigentümer haben, in einer Kette bis hin zur mainFunktion oder zu statischen / globalen Variablen. Ein Objekt zugeordnet Verwendung new, ist nicht im Besitz , bis Sie diese Verantwortung zuweisen, aber intelligente Zeiger besitzen die Halde zugewiesenen Objekte , die sie verweisen, und Behälter besitzen ihre Datenstrukturen. Eigentümer können wählen, vorzeitig zu löschen, der Eigentümer Destruktor ist letztendlich verantwortlich.
Steve314