C++ Core Guidelines: finally in C++

Modernes C++  –  64 Kommentare

In diesem Artikel geht es um die Ausnahmesituation, wenn keine Ausnahme geworfen werden darf. Falls dein Programm in einer eingeschränkten Embedded-Umgebung läuft oder du harte Echtzeitanforderungen sicherstellen musst, ist dies für dich wohl keine Ausnahme.

Lass mich mit der Ausnahmesituation beginnen, in der keine Ausnahme geworfen werden darf. Mein ursprünglicher Plan war es, zumindest die Regeln E.19 bis E.27 der C++ Core Guidelines in diesem Artikel vorzustellen. Leider bin ich an der Regel E.19 hängen geblieben.

E.19: Use a final_action object to express cleanup if no suitable resource handle is available

Die erste Regel mag dich überraschen, denn du hast womöglich noch nie etwas von final_action gehört. Auf mich traf das jedenfalls zu. Daher musste ich recherchieren. Während meiner Suche fand ich den exzellenten Artikel von Bartłomiej Filipek. Er ist Autor des bekannten C++-Blogs: "Bartek's coding blog" und erlaubte mir, seinen Artikel "Beautiful code: final_act from GSL" in meinen einzubetten. Hier ist er ins Deutsche übersetzt:

Manchmal gibt es die Notwendigkeit, ein spezielle Aktion am Ende eines Bereichs zu hinterlegen: Dies kann Code sein, der eine Ressource freigibt, ein Flag setzt oder es sind begin/end-Funktionsaufrufe. Kürzlich fand der Autor ein sehr praktisches Werkzeug, um diesen Job umzusetzen.

Es geht um gsl::final_act/finally.

Einleitung

Den Anschlussartikel gibt es hier.

Angenommen, wir haben den folgenden Code:

void addExtraNodes();
void removeExtraNodes();

bool Scanner::scanNodes()
{
// code...
addExtraNodes();

// code...
removeExtraNodes();
return true;
}

Wir besitzen einige Objekte, die die Funktion scanNodes scannt. Diese Knoten gilt es, zum Container hinzuzufügen. Am Ende soll der ursprüngliche Zustand des Containers wieder gelten, daher müssen die Knoten wieder entfernt werden.

Natürlich könnte der Sourcecode so strukturiert sein, dass die Operation nur auf einer Copy des Container stattfinden würde. In diesem Fall wäre es nicht notwendig, die Knoten zu entfernen. Es gibt aber leider oft Sourcecode, in dem die Aktionen auf einem globalen Container stattfinden und damit die Notwendigkeit besteht, bestimmte Aufräumarbeiten immer auszuführen. Viele Fehler können passieren, wenn eine Aktion auf einem geteilten Container stattfindet und dieser sich nicht in dem erwarteten Zustand befindet.

Der Code hier scheint auf jeden Fall kein Problem zu besitzen. Es wird ja removeExtraNodes am Ende der Funktion aufgerufen. Doch was passiert, wenn die Funktion scanNodes mehrere return-Anweisungen besitzt? Das ist einfach. In diesem Fall müssen nur mehrere Aufrufe removeExtraNodes ausgeführt werden.

Was ist aber nun, wenn die Funktion eine Ausnahme wirft? Dann müssen wir noch die Aufräumarbeiten vor der Ausnahme ausführen. Das ist ganz offensichtlich. Der Aufruf removeExtraNodes muss nicht nur vor der letzten return-Anweisung ausgeführt werden.

Hilfe ist notwendig

Jetzt möchte sich der Autor auf die C++ Core Guidelines beziehen. Sie geben folgenden Rat:

E.19: Use a final_action object to express cleanup if no suitable resource handle is available

Die Guidelines sagen, dass die Software ein besseres Design haben soll als ein goto, gefolgt von einem exit-Aufruf, oder einfach nichts zu tun haben soll. Hier ist der Vorschlag:

bool Scanner::scanNodes()
{
// code...
addExtraNodes();
auto _ = finally([] { removeExtraNodes(); });

// code...

return true;
}

Was passiert hier? Alles was passierte, war nur, den Aufruf removeExtraNodes in ein spezielles Objekt zu verpacken. Dies spezielle Objekt wird die aufrufbare Einheit (Lambda-Funktion) in ihrem Destruktor aufrufen. Das ist genau das, was gesucht wurde!

Wo gibt es den magischen finally()-Code? Hier: Guideline Support Library/gsl_util.h.

Unter der Decke

Der Sourcecode zu final_act ist einfach, daher kann ich ihn hier direkt reinkopieren:

template <class F>
class final_act
{
public:
explicit final_act(F f) noexcept
: f_(std::move(f)), invoke_(true) {}

final_act(final_act&& other) noexcept
: f_(std::move(other.f_)),
invoke_(other.invoke_)
{
other.invoke_ = false;
}

final_act(const final_act&) = delete;
final_act& operator=(const final_act&) = delete;

~final_act() noexcept
{
if (invoke_) f_();
}

private:
F f_;
bool invoke_;
};

Ist der Code nicht wundervoll?

Die Klasse nimmt eine aufrufbare Einheit f_ an und ruft sie dann auf, wenn eine Instanz von ihr destruiert wird. Daher wird immer der Destruktor, der die Aufräumarbeit enthält, aufgerufen, egal ob er jetzt vorzeitig oder durch eine Ausnahme ausgelöst wird.

Um optimal mit der Move-Semantik zusammenzuarbeiten, enthält die Klasse einen zusätzlichen Wahrheitswert invoke_. Dieser stellt sicher, dass der Code für temporäre Objekte nicht ausgeführt wird. Hier gibt es mehr Information dazu: "Final_act copy/move semantics is wrong".

C++17 unterstützt die Template Argument Deduction für Klassen. Damit lässt sich final_act direkt erklären.

final_act _f([] { removeExtraNodes(); });

Vor C++17 erlaubte es die Hilfsfunktion finally, Objekte vom Typ final_act einfach zu erzeugen.

template <class F>
inline final_act<F> finally(const F& f) noexcept
{
return final_act<F>(f);
}

template <class F>
inline final_act<F> finally(F&& f) noexcept
{
return final_act<F>(std::forward<F>(f));
}

Was finde ich nun so schön an dem Code?

  • Der Code ist sehr einfach.
  • Der Code ist ausdrucksreich und benötigt daher keine Kommentare.
  • Der Code übernimmt genau eine Aufgabe.
  • Der Code ist generisch. Er setzt nur eine aufrufbare Einheit voraus: Funktion, Funktionsobjekt oder Lambda-Funktion.
  • Der Code unterstützt modernes C++: Move-Semantik und noexcept.
Wichtige Anmerkung: final_act sollte noexcept sein

Wie es bereits häufig in den Kommentaren zur GSL erklärt wurde, sollte final_act noexcept sein. Dies gilt, da es dazu dient, einen Destruktor aufzurufen, der natürlich noexcept sein soll. Weder soll der Destruktor eine Ausnahme werfen noch final_act. Das mag eine kleine Einschränkung sein, wenn du gewöhnlichen Code mittels final_act aufrufen willst. Gleichzeitig ist dies aber ein Zeichen von schlechtem Design.

Wo kann finally verwendet werden?

Um einen Punkt klarzustellen: Verwende finally nicht zu häufig! Deine Objekte sollten nicht von globalem Zustand abhängen und daher auf dem RAII-Idiom basieren. Trotzdem gibt es Anwendungsfälle, in denen finally zum Einsatz kommen sollte:

  • Transaktionen: Dies ist ein allgemeiner Begriff für Aktionen, die wieder vollkommen rückgängig gemacht werden können, falls ein Fehler auftrat. Das ist zum Beispiel notwendig, wenn ein Fehler beim Schreiben einer Datei auftrat. Diese Aktion soll natürlich rückgängig gemacht werden.
  • begin/end-Funktionen: In diesen ist es notwendig eine end-Aktion zu hinterlegen, nachdem die begin-Aktion ausgeführt wurde. Dies war genau im Beispiel der Fall.
  • Setzen eines Flags: Du hast ein geteiltes Flag, das du auf einen neuen Zustand setzt. Dieses muss aber am Ende der Funktion wieder zurückgesetzt werden.
  • Umgang mit Ressourcen ohne RAII: Falls du deine Ressource nicht in ein RAII-Objekt verpacken kannst, ist final_act eine Lösung.
  • Beenden einer Verbindung: Am Ende der Funktion muss zum Beispiel ein Socket geschlossen werden.

Weitere Informationen

Kürzlich veröffentliche Bartłomiej Filipek sein ersten Buch "C++17 in Detail". Falls du den neuen Standard in einer effektiven und praktischen Art und Weise lernen willst, kannst du sein englisches Buch hier beziehen: https://leanpub.com/cpp17indetail.

Vier Gutscheine für "C++ in Detail"

Bartłomiej Filipek gab mir vier Gutscheine für sein Buch. Hier stehen die Details, wie du einen Gutschein erhalten kannst: "For Free: Four Vouchers to Win".

Fallen dir noch andere Anwendungsfälle für final_act ein? Du kannst diese Liste studieren: "C++ List of ScopeGuard".

Zusammenfassung

Den Anschlussartikel gibt es hier. final_act ist ein wunderschönes und gut entworfenes Werkzeug, das hilft, die hässliche Aufräumarbeit zu erledigen. Du solltest zwar in deinem Code bessere Herangehensweisen wie RAII anwenden, wenn dies aber nicht möglich ist, ist final_act oft die pragmatischste Lösung.

Verwendest du ähnliche Klassen in deinem Sourcecode, um die Aufräumarbeit zu erledigen?

Wie geht's weiter?

Falls du keine Ausnahme werfen darfst und finally auch keine Option ist, hast du ein Problem. In meinem nächsten Artikel werde ich mich damit beschäftigen.