Bei der Entwicklung in C ++ müssen Sie von Zeit zu Zeit Code schreiben, in dem keine Ausnahmen auftreten dürfen. Zum Beispiel, wenn wir einen ausnahmefreien Swap für native Typen schreiben oder eine noexcept move-Anweisung für unsere Klasse definieren oder manuell einen nichttrivialen Destruktor implementieren müssen.
In C ++ 11 wurde der Sprache der Modifikator noexcept hinzugefügt, damit der Entwickler verstehen kann, dass Ausnahmen von der mit noexcept gekennzeichneten Funktion (oder Methode) nicht verworfen werden können. Daher können Funktionen mit einer solchen Markierung sicher in Kontexten verwendet werden, in denen keine Ausnahmen auftreten sollten.
Zum Beispiel, wenn ich diese Typen und Funktionen habe:
class first_resource {...}; class second_resource {...}; void release(first_resource & r) noexcept; void close(second_resource & r);
und es gibt eine bestimmte resources_owner
Klasse, die Objekte wie first_resource
und second_resource
:
class resources_owner { first_resource first_resource_; second_resource second_resource_; ... };
dann kann ich den Destruktor resources_owner
wie folgt schreiben:
resources_owner::~resources_owner() noexcept {
In gewisser Weise hat noexcept in C ++ 11 das Leben eines C ++ - Entwicklers erleichtert. Aber die derzeitige Noexcept-Implementierung in modernem C ++ hat eine böse Seite ...
Der Compiler hilft nicht dabei, den Inhalt von noexcept-Funktionen und -Methoden zu steuern
Angenommen, ich habe mich im obigen Beispiel geirrt: Aus irgendeinem Grund habe ich release()
als noexcept markiert, aber in Wirklichkeit ist dies keine Ausnahme und kann Ausnahmen auslösen. Dies bedeutet, dass, wenn ich einen Destruktor mit einem solchen Wurf release()
schreibe:
resources_owner::~resources_owner() noexcept { release(first_resource_);
dann bitte ich um Ärger. Früher oder später wird diese release()
eine Ausnahme auslösen und meine gesamte Anwendung wird aufgrund des automatisch aufgerufenen std::terminate()
abstürzen. Es wird noch schlimmer sein, wenn nicht meine Anwendung abstürzt, sondern die einer anderen, in der sie meine Bibliothek mit einem so problematischen Destruktor für resources_owner
.
Oder eine andere Variante des gleichen Problems. Angenommen, ich habe mich nicht geirrt, dass release()
tatsächlich als noexcept markiert ist. Es war.
Es wurde in Version 1.0 einer Drittanbieter-Bibliothek markiert, aus der ich first_resource
und release()
first_resource
. Und dann, nach einigen Jahren, habe ich auf Version 3.0 dieser Bibliothek aktualisiert, aber in Version 3.0 hat release()
keinen noexcept-Modifikator mehr.
Nun, was? In der neuen Hauptversion könnten sie die API leicht beschädigen.
Erst jetzt werde ich höchstwahrscheinlich vergessen, die Implementierung des Destruktors resources_owner
korrigieren. Und wenn anstelle von mir jemand anderes an der Unterstützung von resource_owner
, der diesen Destruktor nie untersucht hat, bleiben die Änderungen in der release()
Signatur wahrscheinlich unbemerkt.
Daher gefällt mir persönlich die Tatsache nicht, dass der Compiler den Programmierer in keiner Weise warnt, dass der Programmierer in der Methode / Funktion noexcept einen ausnahmefreudigen Methoden- / Funktionsaufruf ausführt.
Es wäre besser, wenn der Compiler solche Warnungen ausgeben würde.
Die Rettung des Ertrinkens ist die Arbeit des Ertrinkens selbst
OK, der Compiler gibt keine Warnungen aus. Und gegen diesen einfachen Entwickler kann nichts unternommen werden. Nehmen Sie keine Änderungen am C ++ - Compiler für Ihre eigenen Bedürfnisse vor. Insbesondere, wenn Sie nicht einen Compiler verwenden müssen, sondern verschiedene Versionen verschiedener C ++ - Compiler.
Ist es möglich, Hilfe vom Compiler zu erhalten, ohne in seine Innereien zu geraten? Das heißt, Ist es möglich, Werkzeuge zur Steuerung des Inhalts von Methoden / Funktionen ohne Ausnahme zu erstellen, selbst wenn es sich um die dendro-fäkale Methode handelt?
Du kannst. Schlampig, aber möglich.
Woher wachsen die Beine?
Der in diesem Artikel beschriebene Ansatz wurde in der Praxis getestet, als die nächste Version unseres kleinen eingebetteten HTTP-Servers RESTinio vorbereitet wurde .
Tatsache ist, dass wir die Ausnahmesicherheitsprobleme an mehreren Stellen aus den Augen verloren haben, da RESTinio mit Funktionen gefüllt ist. Insbesondere wurde im Laufe der Zeit klar, dass Ausnahmen manchmal von Rückrufen ausgehen können, die an Asio übertragen wurden (was nicht der Fall sein sollte), und dass Ausnahmen im Prinzip beim Reinigen von Ressourcen auftreten können.
Glücklicherweise haben sich diese Probleme in der Praxis nie manifestiert, aber die technischen Schulden haben sich angesammelt und es musste etwas dagegen unternommen werden. Und Sie mussten etwas mit dem Code tun, der bereits geschrieben wurde. Das heißt, funktionierender nicht-noexcept-Code sollte in funktionierenden noexcept-Code konvertiert werden.
Dies geschah mit Hilfe mehrerer Makros, die per Code an den richtigen Stellen angeordnet waren. Zum Beispiel ein trivialer Fall:
template< typename Message_Builder > void trigger_error_and_close( Message_Builder msg_builder ) noexcept {
Und hier ist ein weniger triviales Fragment:
void reset() noexcept { RESTINIO_STATIC_ASSERT_NOEXCEPT(m_context_table.empty()); RESTINIO_STATIC_ASSERT_NOEXCEPT( m_context_table.pop_response_context_nonchecked()); RESTINIO_STATIC_ASSERT_NOEXCEPT(m_context_table.front()); RESTINIO_STATIC_ASSERT_NOEXCEPT(m_context_table.front().dequeue_group()); RESTINIO_STATIC_ASSERT_NOEXCEPT(make_asio_compaible_error( asio_convertible_error_t::write_was_not_executed)); for(; !m_context_table.empty(); m_context_table.pop_response_context_nonchecked() ) { const auto ec = make_asio_compaible_error( asio_convertible_error_t::write_was_not_executed ); auto & current_ctx = m_context_table.front(); while( !current_ctx.empty() ) { auto wg = current_ctx.dequeue_group(); restinio::utils::suppress_exceptions_quietly( [&] { wg.invoke_after_write_notificator_if_exists( ec ); } ); } } }
Die Verwendung dieser Makros gab mir mehrmals die Hand und zeigte auf Orte, die ich versehentlich als nicht wahrgenommen hatte, die es aber nicht waren.
Der unten beschriebene Ansatz ist natürlich ein selbst hergestellter Ansatz mit quadratischen Rädern, aber es geht ... Ich meine, es funktioniert.
Weiter im Artikel werden wir die Implementierung, die vom RESTinio-Code isoliert wurde, in einen separaten Satz von Makros diskutieren.
Das Wesentliche des Ansatzes
Das Wesentliche des Ansatzes besteht darin, die Anweisung / den Operator (stmt), die auf noexcept überprüft werden muss, an ein bestimmtes Makro zu übergeben. Dieses Makro verwendet static_assert(noexcept(stmt), msg)
, um zu überprüfen, ob stmt wirklich noexcept ist, und ersetzt dann stmt im Code.
Im Wesentlichen ist dies:
ENSURE_NOEXCEPT_STATEMENT(release(some_resource));
wird ersetzt durch so etwas wie:
static_assert(noexcept(release(some_resource)), "release(some_resource) is expected to be noexcept"); release(some_resource);
Warum wurde die Wahl zugunsten von Makros getroffen?
Im Prinzip könnte man auf Makros verzichten und static_assert(noexcept(...))
direkt in den Code schreiben, unmittelbar bevor die Aktionen überprüft werden. Aber Makros haben mindestens ein paar Vorteile, die den Ausschlag für die gezielte Verwendung von Makros geben.
Erstens reduzieren Makros die Codeduplizierung. Es gibt einen Vergleich:
static_assert(noexcept(release(some_resource)), "release(some_resource) is expected to be noexcept"); release(some_resource);
und
ENSURE_NOEXCEPT_STATEMENT(release(some_resource));
es ist klar, dass bei Makros der Hauptausdruck, d.h. release(some_resource)
kann nur einmal geschrieben werden. Dies verringert die Wahrscheinlichkeit, dass der Code im Laufe der Zeit mit seiner Begleitung "kriecht", wenn eine Korrektur an einer Stelle vorgenommen und an der zweiten vergessen wurde.
Zweitens können Makros und dementsprechend die dahinter verborgenen Prüfungen sehr einfach deaktiviert werden. Sagen wir, wenn die Fülle von static_assert-s die Kompilierungsgeschwindigkeit nachteilig beeinflusst (obwohl ich einen solchen Effekt nicht bemerkt habe). Noch wichtiger ist, dass beim Aktualisieren einer Bibliothek eines Drittanbieters Kompilierungsfehler von static_assert, die hinter Makros versteckt sind, direkt mit dem Fluss besprüht werden können. Das vorübergehende Deaktivieren von Makros kann eine reibungslose Aktualisierung des Codes ermöglichen, einschließlich Überprüfungsmakros nacheinander zuerst in einer Datei, dann in der zweiten, dann in der dritten usw.
Obwohl Makros in C ++ eine veraltete und höchst kontroverse Funktion sind, wird in diesem speziellen Fall das Leben des Entwicklers vereinfacht.
Hauptmakro ENSURE_NOEXCEPT_STATEMENT
Das Hauptmakro ENSURE_NOEXCEPT_STATEMENT ist trivial implementiert:
#define ENSURE_NOEXCEPT_STATEMENT(stmt) \ do { \ static_assert(noexcept(stmt), "this statement is expected to be noexcept: " #stmt); \ stmt; \ } while(false)
Es wird verwendet, um zu überprüfen, ob die aufgerufenen Methoden / Funktionen tatsächlich keine Ausnahme sind und dass ihre Aufrufe nicht von Try-Catch-Blöcken umrahmt werden müssen. Zum Beispiel:
class some_complex_container { one_container first_data_part_; another_container second_data_part_; ... public: friend void swap(some_complex_container & a, some_complex_container & b) noexcept { using std::swap;
Darüber hinaus gibt es das Makro ENSURE_NOT_NOEXCEPT_STATEMENT. Es wird verwendet, um sicherzustellen, dass um den Aufruf herum ein zusätzlicher Try-Catch-Block erforderlich ist, damit mögliche Ausnahmen nicht herausfliegen:
class some_resource_owner { some_resource resource_; ... public: ~some_resource_owner() noexcept { try {
Hilfsmakros STATIC_ASSERT_NOEXCEPT und STATIC_ASSERT_NOT_NOEXCEPT
Leider können die Makros ENSURE_NOEXCEPT_STATEMENT und ENSURE_NOT_NOEXCEPT_STATEMENT nur für Anweisungen / Anweisungen verwendet werden, nicht jedoch für Ausdrücke, die einen Wert zurückgeben. Das heißt, Sie können mit ENSURE_NOEXCEPT_STATEMENT nicht wie folgt schreiben:
auto resource = ENSURE_NOEXCEPT_STATEMENT(acquire_resource(params));
Daher kann ENSURE_NOEXCEPT_STATEMENT nicht verwendet werden, z. B. in Schleifen, in denen Sie häufig Folgendes schreiben müssen:
for(auto i = something.get_first(); i != some_other_object; i = i.get_next()) {...}
und Sie müssen sicherstellen, dass die Aufrufe get_first()
, get_next()
sowie die Zuweisung neuer Werte für i keine Ausnahme get_next()
.
Um solchen Situationen entgegenzuwirken, wurden die Makros STATIC_ASSERT_NOEXCEPT und STATIC_ASSERT_NOT_NOEXCEPT geschrieben, hinter denen nur static_asserts versteckt sind und nichts weiter. Mit diesen Makros kann ich auf irgendeine Weise das gewünschte Ergebnis erzielen (die Kompilierung dieses bestimmten Fragments wurde nicht überprüft):
STATIC_ASSERT_NOEXCEPT(something.get_first()); STATIC_ASSERT_NOEXCEPT(something.get_first().get_next()); STATIC_ASSERT_NOEXCEPT(std::declval<decltype(something.get_first())>() = something.get_first().get_next()); for(auto i = something.get_first(); i != some_other_object; i = i.get_next()) {...}
Offensichtlich ist dies nicht die beste Lösung, weil Dies führt zu einer Duplizierung des Codes und erhöht das Risiko seines "Kriechens" bei weiterer Wartung. In einem ersten Schritt erwiesen sich diese einfachen Makros jedoch als nützlich.
Noexcept-ctcheck-Bibliothek
Als ich diese Erfahrung in meinem Blog und auf Facebook teilte, erhielt ich einen Vorschlag, die oben genannten Entwicklungen in einer separaten Bibliothek anzuordnen. Was getan wurde: github hat jetzt eine winzige Bibliothek, die nur für Header gedacht ist. Noexcept-compile-time-check (oder noexcept-ctcheck, wenn Sie Buchstaben sparen) . Sie können also alles oben Genannte ausprobieren. Die Namen der Makros sind zwar etwas länger als im Artikel verwendet. Das heißt, NOEXCEPT_CTCHECK_ENSURE_NOEXCEPT_STATEMENT anstelle von ENSURE_NOEXCEPT_STATEMENT.
Was ist (noch?) Nicht in noexcept-ctcheck eingeflossen?
Es besteht der Wunsch, das Makro ENSURE_NOEXCEPT_EXPRESSION zu erstellen, das wie folgt verwendet werden könnte:
auto resource = ENSURE_NOEXCEPT_EXPRESSION(acquire_resource(params));
In erster Näherung könnte er ungefähr so aussehen:
#define ENSURE_NOEXCEPT_EXPRESSION(expr) \ ([&]() noexcept -> decltype(auto) { \ static_assert(noexcept(expr), #expr " is expected to be noexcept"); \ return expr; \ }())
Aber es gibt vage Vermutungen, dass es einige Fallstricke gibt, über die ich nicht nachgedacht habe. Im Allgemeinen haben die Hände ENSURE_NOEXCEPT_EXPRESSION :( noch nicht erreicht
Und wenn du träumst?
Mein alter Traum ist es, einen Noexcept-Block in C ++ zu bekommen, in dem der Compiler selbst nach Ausnahmen sucht und Warnungen ausgibt, wenn Ausnahmen ausgelöst werden können. Es scheint mir, dass dies das Schreiben von ausnahmesicherem Code erleichtern würde. Und das nicht nur in den oben genannten offensichtlichen Fällen (Swap, Move-Operatoren, Destruktoren). Zum Beispiel könnte ein Noexcept-Block in dieser Situation helfen:
void modify_some_complex_data() {
Für die Richtigkeit des Codes ist es hier sehr wichtig, dass die in noexcept-Blöcken ausgeführten Aktionen keine Ausnahmen auslösen. Und wenn der Compiler dies verfolgen kann, ist dies eine ernsthafte Hilfe für den Entwickler.
Aber vielleicht ist ein Noexcept-Block nur ein Sonderfall eines allgemeineren Problems. Nämlich: Überprüfen der Erwartungen des Programmierers, dass ein Codeblock bestimmte Eigenschaften hat. Sei es das Fehlen von Ausnahmen, das Fehlen von Nebenwirkungen, das Fehlen von Rekursion, Datenrennen usw.
Überlegungen zu diesem Thema vor einigen Jahren führten zu der Idee, Attribute zu implizieren und zu erwarten . Diese Idee ging nicht weiter als der Blog-Beitrag, weil während sie sich von meinen aktuellen Interessen und Möglichkeiten fernhält. Aber plötzlich wird es für jemanden interessant sein und jemand wird darauf drängen, etwas Lebensfähigeres zu schaffen.
Fazit
In diesem Artikel habe ich versucht, über meine Erfahrungen bei der Vereinfachung des Schreibens von ausnahmesicherem Code zu sprechen. Die Verwendung von Makros macht den Code natürlich nicht schöner und kompakter. Aber es funktioniert. Und selbst solche primitiven Makros erhöhen den Koeffizienten meines erholsamen Schlafes ganz erheblich. Wenn jemand anderes nicht darüber nachgedacht hat, wie er den Inhalt seiner eigenen noexcept-Methoden / -Funktionen steuern kann, wird dieser Artikel Sie möglicherweise dazu inspirieren, über dieses Thema nachzudenken.
Und wenn jemand einen Weg gefunden hat, sein Leben beim Schreiben von Noexcept-Code zu vereinfachen, wäre es interessant zu wissen, was diese Methode ist, bei der sie hilft und bei der sie nicht hilft. Und wie zufrieden sind Sie mit dem, was Sie verwenden.