Ich bin überrascht, dass niemand diese Alternative vorgeschlagen hat. Obwohl die Frage schon eine Weile besteht, füge ich sie hinzu: Eine gute Möglichkeit, dieses Problem zu beheben, besteht darin, Variablen zu verwenden, um den aktuellen Status zu verfolgen. Dies ist eine Technik, die verwendet werden kann, unabhängig davon, ob goto
zum Auffinden des Bereinigungscodes verwendet wird oder nicht . Wie jede Codierungstechnik hat sie Vor- und Nachteile und ist nicht für jede Situation geeignet. Wenn Sie sich jedoch für einen Stil entscheiden, sollten Sie darüber nachdenken - insbesondere, wenn Sie vermeiden möchten, goto
ohne tief verschachtelte if
s zu erhalten.
Die Grundidee ist, dass es für jede Bereinigungsaktion, die möglicherweise ausgeführt werden muss, eine Variable gibt, anhand deren Wert wir erkennen können, ob die Bereinigung durchgeführt werden muss oder nicht.
Ich werde zuerst die goto
Version zeigen , da sie näher am Code in der ursprünglichen Frage liegt.
int foo(int bar)
{
int return_value = 0;
int something_done = 0;
int stuff_inited = 0;
int stuff_prepared = 0;
/*
* Prepare
*/
if (do_something(bar)) {
something_done = 1;
} else {
goto cleanup;
}
if (init_stuff(bar)) {
stuff_inited = 1;
} else {
goto cleanup;
}
if (prepare_stuff(bar)) {
stufF_prepared = 1;
} else {
goto cleanup;
}
/*
* Do the thing
*/
return_value = do_the_thing(bar);
/*
* Clean up
*/
cleanup:
if (stuff_prepared) {
unprepare_stuff();
}
if (stuff_inited) {
uninit_stuff();
}
if (something_done) {
undo_something();
}
return return_value;
}
Ein Vorteil gegenüber einigen anderen Techniken besteht darin, dass, wenn die Reihenfolge der Initialisierungsfunktionen geändert wird, die korrekte Bereinigung weiterhin erfolgt - beispielsweise unter Verwendung der switch
in einer anderen Antwort beschriebenen Methode, wenn sich die Reihenfolge der Initialisierung ändert, dann die switch
muss sehr sorgfältig bearbeitet werden, um zu vermeiden, dass versucht wird, etwas zu bereinigen, das überhaupt nicht initialisiert wurde.
Einige mögen nun argumentieren, dass diese Methode eine ganze Reihe zusätzlicher Variablen hinzufügt - und in diesem Fall ist dies tatsächlich der Fall -, aber in der Praxis verfolgt eine vorhandene Variable häufig bereits den erforderlichen Zustand oder kann dazu gebracht werden, diesen zu verfolgen. Wenn dies beispielsweise prepare_stuff()
tatsächlich ein Aufruf von malloc()
oder an ist open()
, kann die Variable verwendet werden, die den zurückgegebenen Zeiger oder Dateideskriptor enthält - zum Beispiel:
int fd = -1;
....
fd = open(...);
if (fd == -1) {
goto cleanup;
}
...
cleanup:
if (fd != -1) {
close(fd);
}
Wenn wir nun zusätzlich den Fehlerstatus mit einer Variablen verfolgen, können wir dies goto
vollständig vermeiden und trotzdem korrekt bereinigen, ohne dass Einrückungen vorhanden sind, die immer tiefer werden, je mehr Initialisierung wir benötigen:
int foo(int bar)
{
int return_value = 0;
int something_done = 0;
int stuff_inited = 0;
int stuff_prepared = 0;
int oksofar = 1;
/*
* Prepare
*/
if (oksofar) { /* NB This "if" statement is optional (it always executes) but included for consistency */
if (do_something(bar)) {
something_done = 1;
} else {
oksofar = 0;
}
}
if (oksofar) {
if (init_stuff(bar)) {
stuff_inited = 1;
} else {
oksofar = 0;
}
}
if (oksofar) {
if (prepare_stuff(bar)) {
stuff_prepared = 1;
} else {
oksofar = 0;
}
}
/*
* Do the thing
*/
if (oksofar) {
return_value = do_the_thing(bar);
}
/*
* Clean up
*/
if (stuff_prepared) {
unprepare_stuff();
}
if (stuff_inited) {
uninit_stuff();
}
if (something_done) {
undo_something();
}
return return_value;
}
Auch hier gibt es mögliche Kritikpunkte:
- Verletzen nicht all diese "Wenn" die Leistung? Nein - denn im Erfolgsfall müssen Sie sowieso alle Überprüfungen durchführen (andernfalls überprüfen Sie nicht alle Fehlerfälle). und im
if (oksofar)
Fehlerfall optimieren die meisten Compiler die Reihenfolge der fehlgeschlagenen Überprüfungen auf einen einzigen Sprung zum Bereinigungscode (GCC sicherlich) - und in jedem Fall ist der Fehlerfall normalerweise weniger kritisch für die Leistung.
Fügt dies nicht noch eine weitere Variable hinzu? In diesem Fall ja, aber oft kann die return_value
Variable verwendet werden, um die Rolle zu spielen, die oksofar
hier spielt. Wenn Sie Ihre Funktionen so strukturieren, dass Fehler konsistent zurückgegeben werden, können Sie sogar die zweite if
in jedem Fall vermeiden :
int return_value = 0;
if (!return_value) {
return_value = do_something(bar);
}
if (!return_value) {
return_value = init_stuff(bar);
}
if (!return_value) {
return_value = prepare_stuff(bar);
}
Einer der Vorteile einer solchen Codierung besteht darin, dass die Konsistenz bedeutet, dass jeder Ort, an dem der ursprüngliche Programmierer vergessen hat, den Rückgabewert zu überprüfen, wie ein schmerzender Daumen hervorsteht, was das Auffinden (dieser einen Klasse von) Fehlern erheblich erleichtert.
Also - dies ist (noch) ein weiterer Stil, mit dem dieses Problem gelöst werden kann. Bei richtiger Anwendung ermöglicht es sehr sauberen, konsistenten Code - und wie bei jeder Technik kann es in den falschen Händen dazu führen, dass Code erzeugt wird, der langwierig und verwirrend ist :-)