Deterministische Ausnahmen und Fehlerbehandlung in „C ++ der Zukunft“


Es ist seltsam, dass auf Habrt noch kein lauter Vorschlag für den C ++ - Standard namens "Zero-Overhead-deterministische Ausnahmen" erwähnt wurde. Korrigieren Sie diese nervige Auslassung.


Wenn Sie sich Sorgen über den Overhead von Ausnahmen machen oder Code ohne Ausnahmeunterstützung kompilieren mussten oder sich nur fragen, was mit der Fehlerbehandlung in C ++ 2b passieren wird (ein Verweis auf einen kürzlich veröffentlichten Beitrag ), frage ich nach cat. Sie warten auf einen Druck auf alles, was jetzt zu diesem Thema zu finden ist, und auf ein paar Umfragen.


Die folgende Diskussion wird nicht nur über statische Ausnahmen geführt, sondern auch über verwandte Vorschläge zum Standard und über alle möglichen anderen Möglichkeiten, mit Fehlern umzugehen. Wenn Sie hierher gekommen sind, um sich die Syntax anzusehen, dann ist es hier:


double safe_divide(int x, int y) throws(arithmetic_error) { if (y == 0) { throw arithmetic_error::divide_by_zero; } else { return as_double(x) / y; } } void caller() noexcept { try { cout << safe_divide(5, 2); } catch (arithmetic_error e) { cout << e; } } 

Wenn der spezifische Fehlertyp unwichtig / unbekannt ist, können Sie einfach throws and catch (std::error e) .


Gut zu wissen


std::optional und std::expected


Lassen Sie uns entscheiden, dass der Fehler, der möglicherweise in der Funktion auftreten kann, nicht „schwerwiegend“ genug ist, um eine Ausnahme auszulösen. Traditionell werden Fehlerinformationen mithilfe eines out-Parameters zurückgegeben. Beispielsweise bietet das Dateisystem TS eine Reihe ähnlicher Funktionen:


 uintmax_t file_size(const path& p, error_code& ec); 

(Keine Ausnahme auslösen, da die Datei nicht gefunden wurde?) Die Verarbeitung von Fehlercodes ist jedoch umständlich und fehleranfällig. Der Fehlercode kann leicht vergessen werden. Moderne Codestile verbieten die Verwendung von Ausgabeparametern. Stattdessen wird empfohlen, eine Struktur zurückzugeben, die das gesamte Ergebnis enthält.


Boost bietet seit einiger Zeit eine elegante Lösung für die Behandlung solcher „nicht schwerwiegenden“ Fehler, die in bestimmten Szenarien im richtigen Programm zu Hunderten auftreten können:


 expected<uintmax_t, error_code> file_size(const path& p); 

Der expected Typ ähnelt der variant , bietet jedoch eine praktische Schnittstelle für die Arbeit mit "Ergebnis" und "Fehler". Standardmäßig wird das expected Ergebnis in expected gespeichert. Die Implementierung von file_size könnte file_size aussehen:


 file_info* info = read_file_info(p); if (info != null) { uintmax_t size = info->size; return size; // <== } else { error_code error = get_error(); return std::unexpected(error); // <== } 

Wenn die Fehlerursache für uns nicht interessant ist oder der Fehler nur in der „Abwesenheit“ des Ergebnisses bestehen kann, kann optional verwendet werden:


 optional<int> parse_int(const std::string& s); optional<U> get_or_null(map<T, U> m, const T& key); 

In C ++ 17 von Boost kam optional zu std (ohne Unterstützung für optional<T&> ); In C ++ 20 können sie erwartete hinzufügen (dies ist nur Vorschlag, danke RamzesXI für die Korrektur).


Verträge


Verträge (nicht zu verwechseln mit Konzepten) sind eine neue Möglichkeit, Funktionsparameter einzuschränken, die in C ++ 20 hinzugefügt wurden. 3 Anmerkungen hinzugefügt:


  • erwartet prüft Funktionsparameter
  • stellt sicher , dass der Rückgabewert der Funktion überprüft wird (nimmt ihn als Argument)
  • assert - ein zivilisierter Ersatz für das Makro assert

 double unsafe_at(vector<T> v, size_t i) [[expects: i < v.size()]]; double sqrt(double x) [[expects: x >= 0]] [[ensures ret: ret >= 0]]; value fetch_single(key e) { vector<value> result = fetch(vector<key>{e}); [[assert result.size() == 1]]; return v[0]; } 

Sie können für Vertragsbruch konfigurieren:


  • Undefiniertes Verhalten genannt, oder
  • Es überprüfte und rief den User-Exit auf, wonach std::terminate

Es ist unmöglich, das Programm nach Vertragsbruch weiter auszuführen, da Compiler Garantien aus Verträgen verwenden, um den Funktionscode zu optimieren. Im geringsten Zweifel an der Vertragserfüllung lohnt es sich, einen zusätzlichen Scheck hinzuzufügen.


std :: error_code


Mit der in C ++ 11 hinzugefügten Bibliothek <system_error> können Sie die Behandlung von Fehlercodes in Ihrem Programm standardisieren. std :: error_code besteht aus einem Fehlercode vom Typ int und einem Zeiger auf das Objekt einer untergeordneten Klasse std :: error_category . Dieses Objekt spielt tatsächlich die Rolle einer Tabelle virtueller Funktionen und bestimmt das Verhalten eines bestimmten std::error_code .


Um Ihren std::error_code zu erstellen, müssen Sie std::error_category Nachkommenklasse std::error_category definieren und virtuelle Methoden implementieren. Die wichtigste davon ist:


 virtual std::string message(int c) const = 0; 

Sie müssen auch eine globale Variable für Ihre std::error_category . Die Fehlerbehandlung mit error_code + expected sieht ungefähr so ​​aus:


 template <typename T> using result = expected<T, std::error_code>; my::file_handle open_internal(const std::string& name, int& error); auto open_file(const std::string& name) -> result<my::file> { int raw_error = 0; my::file_handle maybe_result = open_internal(name, &raw_error); std::error_code error{raw_error, my::filesystem_error}; if (error) { return unexpected{error}; } else { return my::file{maybe_result}; } } 

Es ist wichtig, dass in std::error_code Wert von 0 keinen Fehler bedeutet. Wenn dies bei Ihren Fehlercodes nicht der Fall ist, müssen Sie vor der Konvertierung des Systemfehlercodes in std::error_code den Code 0 durch SUCCESS ersetzen und umgekehrt.


Alle Systemfehlercodes werden in errc und system_category beschrieben . Wenn zu einem bestimmten Zeitpunkt die manuelle Weiterleitung der Fehlercodes zu trostlos wird, können Sie den Fehlercode jederzeit in die std::system_error und wegwerfen.


Zerstörerische Bewegung / Trivial umsetzbar


Sie müssen eine andere Klasse von Objekten erstellen, die einige Ressourcen besitzen. Höchstwahrscheinlich möchten Sie es nicht kopierbar, sondern verschiebbar machen, da die Arbeit mit nicht verschiebbaren Objekten unpraktisch ist (vor C ++ 17 konnten sie nicht von einer Funktion zurückgegeben werden).


Aber hier ist das Problem: In jedem Fall muss das verschobene Objekt gelöscht werden. Daher ist ein spezieller Status "verschoben von" erforderlich, dh ein "leeres" Objekt, das nichts löscht. Es stellt sich heraus, dass jede C ++ - Klasse einen leeren Zustand haben muss, dh es ist unmöglich, eine Klasse mit einer Invariante (Garantie) der Korrektheit vom Konstruktor zum Destruktor zu erstellen. Beispielsweise ist es nicht möglich, die richtige open_file Klasse einer Datei zu erstellen, die während ihrer gesamten Lebensdauer geöffnet ist. Es ist seltsam, dies in einer der wenigen Sprachen zu beobachten, die RAII aktiv verwenden.


Ein weiteres Problem ist das Nullstellen alter Objekte beim Verschieben, was einen zusätzlichen Aufwand verursacht: Das Füllen von std::vector<std::unique_ptr<T>> kann bis zu zweimal langsamer sein als std::vector<T*> da alte Zeiger beim Verschieben auf Null gesetzt werden , gefolgt von der Entfernung von Dummies.


C ++ - Entwickler haben lange an Rust geleckt, wo Destruktoren nicht für verschobene Objekte aufgerufen werden. Diese Funktion wird als destruktiver Zug bezeichnet. Leider bietet Proposal Trivially relocatable nicht an, es zu C ++ hinzuzufügen. Das Overhead-Problem wird jedoch gelöst.


Eine Klasse wird als trivial verschiebbar angesehen, wenn zwei Vorgänge: Verschieben und Löschen des alten Objekts gleichbedeutend mit memcpy vom alten zum neuen Objekt sind. Das alte Objekt wird nicht gelöscht, die Autoren nennen es "auf den Boden fallen lassen".


Ein Typ ist aus Compilersicht trivial verschiebbar, wenn eine der folgenden (rekursiven) Bedingungen erfüllt ist:


  1. Es ist trivial beweglich + trivial zerstörbar (z. B. int oder POD-Struktur)
  2. Dies ist die Klasse, die mit dem Attribut [[trivially_relocatable]]
  3. Dies ist eine Klasse, deren Mitglieder alle trivial umsetzbar sind.

Sie können diese Informationen mit std::uninitialized_relocate , das move init + delete auf die übliche Weise ausführt oder wenn möglich beschleunigt. Es wird empfohlen, die meisten Typen der Standardbibliothek als [[trivially_relocatable]] zu markieren, einschließlich std::string , std::vector , std::unique_ptr . Overhead std::vector<std::unique_ptr<T>> diesem Hintergrund wird der Vorschlag verschwinden.


Was ist jetzt mit Ausnahmen los?


Der C ++ - Ausnahmemechanismus wurde 1992 entwickelt. Es wurden verschiedene Implementierungsoptionen vorgeschlagen. Von diesen wurde ein Ausnahmetabellenmechanismus ausgewählt, der das Fehlen eines Overheads für den Hauptpfad der Programmausführung garantiert. Denn vom Moment ihrer Entstehung an wurde angenommen, dass Ausnahmen sehr selten ausgelöst werden sollten .


Nachteile dynamischer (d. H. Regelmäßiger) Ausnahmen:


  1. Im Fall der ausgelösten Ausnahme beträgt der Overhead im Durchschnitt etwa 10.000 bis 100.000 CPU-Zyklen und kann im schlimmsten Fall die Größenordnung von Millisekunden erreichen
  2. Erhöhung der Binärdateigröße um 15-38%
  3. Inkompatibilität mit C-Programmierschnittstelle
  4. Unterstützung für implizite Ausnahmewürfe in allen Funktionen außer noexcept . Eine Ausnahme kann fast überall im Programm ausgelöst werden, auch wenn der Funktionsautor dies nicht erwartet

Aufgrund dieser Mängel ist der Umfang der Ausnahmen erheblich eingeschränkt. Wenn Ausnahmen nicht gelten können:


  1. Wo Determinismus wichtig ist, dh wo es nicht akzeptabel ist, dass der Code "manchmal" 10, 100, 1000 Mal langsamer als gewöhnlich arbeitet
  2. Wenn sie in ABI nicht unterstützt werden, z. B. in Mikrocontrollern
  3. Wenn ein großer Teil des Codes in C geschrieben ist
  4. In Unternehmen mit einer großen Menge an Legacy-Code ( Google Style Guide , Qt ). Wenn der Code mindestens eine nicht ausnahmesichere Funktion enthält, wird gemäß dem Gesetz der Gemeinheit früher oder später eine Ausnahme durch den Code geworfen und ein Fehler verursacht
  5. In Unternehmen, die Programmierer einstellen, die keine Ahnung von Ausnahmesicherheit haben

Umfragen zufolge sind an den Arbeitsplätzen von 52% (!) Entwicklern Ausnahmen nach Unternehmensregeln verboten.


Ausnahmen sind jedoch ein wesentlicher Bestandteil von C ++! Durch das -fno-exceptions des -fno-exceptions verlieren Entwickler die Möglichkeit, einen wesentlichen Teil der Standardbibliothek zu verwenden. Dies regt Unternehmen außerdem dazu an, ihre eigenen „Standardbibliotheken“ einzurichten und ihre eigene String-Klasse zu erfinden.


Dies ist jedoch nicht das Ende. Ausnahmen sind die einzige Standardmethode, um die Erstellung eines Objekts im Konstruktor abzubrechen und einen Fehler auszulösen. Wenn sie ausgeschaltet sind, erscheint ein Gräuel wie eine zweiphasige Initialisierung. Bediener können auch keine Fehlercodes verwenden, daher werden sie durch Funktionen wie assign .


Vorschlag: Ausnahmen der Zukunft


Neuer Ausnahmeübertragungsmechanismus


Herb Sutter in P709 beschrieb einen neuen Mechanismus zur Übertragung von Ausnahmen. Im Prinzip gibt die Funktion std::expected . Anstelle eines separaten Diskriminators vom Typ bool , der zusammen mit der Ausrichtung bis zu 8 Byte auf dem Stapel belegt, wird dieses Informationsbit jedoch schneller an Carry Flag übertragen.


Funktionen, die CF nicht berühren (die meisten von ihnen), erhalten die Möglichkeit, statische Ausnahmen kostenlos zu verwenden - sowohl bei einer normalen Rückgabe als auch bei einer Ausnahme! Funktionen, die zum Speichern und Wiederherstellen gezwungen sind, erhalten einen minimalen Overhead und sind weiterhin schneller als std::expected und alle normalen Fehlercodes.


Statische Ausnahmen sehen folgendermaßen aus:


 int safe_divide(int i, int j) throws(arithmetic_errc) { if (j == 0) throw arithmetic_errc::divide_by_zero; if (i == INT_MIN && j == -1) throw arithmetic_errc::integer_divide_overflows; return i / j; } double foo(double i, double j, double k) throws(arithmetic_errc) { return i + safe_divide(j, k); } double bar(int i, double j, double k) { try { cout << foo(i, j, k); } catch (erithmetic_errc e) { cout << e; } } 

In der alternativen Version wird vorgeschlagen, das Schlüsselwort try im selben Ausdruck wie den Funktionsaufruf try i + safe_divide(j, k) zu verpflichten: try i + safe_divide(j, k) . Dadurch wird die Anzahl der Fälle, in denen throws in Code verwendet werden, der für Ausnahmen nicht sicher ist, auf nahezu Null reduziert. Im Gegensatz zu dynamischen Ausnahmen kann die IDE in jedem Fall Ausdrücke hervorheben, die Ausnahmen auslösen.


Die Tatsache, dass die ausgelöste Ausnahme nicht separat gespeichert wird, sondern direkt an die Stelle des zurückgegebenen Werts gesetzt wird, führt zu Einschränkungen hinsichtlich der Art der Ausnahme. Erstens muss es trivial verlagerbar sein. Zweitens sollte seine Größe nicht sehr groß sein (aber es kann so etwas wie std::unique_ptr ), sonst reservieren alle Funktionen mehr Platz auf dem Stapel.


status_code


Die von Niall Douglas entwickelte Bibliothek <system_error2> enthält den status_code<T> - "neuen, besseren" error_code . Die Hauptunterschiede zu error_code :


  1. status_code - Ein Vorlagentyp, mit dem fast alle denkbaren Fehlercodes (zusammen mit einem Zeiger auf status_code_category ) ohne statische Ausnahmen status_code_category werden können
  2. T sollte trivial verlagerbar und kopierbar sein (letzteres, IMHO, sollte nicht obligatorisch sein). Beim Kopieren und Löschen werden virtuelle Funktionen aus status_code_category
  3. status_code kann nicht nur status_code speichern, sondern auch zusätzliche Informationen zu einem erfolgreich abgeschlossenen Vorgang
  4. Die "virtuelle" Funktion code.message() gibt nicht std::string , aber string_ref ist ein ziemlich schwerer String-Typ, der eine virtuelle "möglicherweise besitzende" std::string_view . Dort können Sie string_view oder string oder std::shared_ptr<string> oder eine andere verrückte Art, einen String zu besitzen, string_view . Niall behauptet, dass #include <string> den Header <system_error2> unannehmbar "schwer" machen würde.

Als nächstes wird errored_status_code<T> eingegeben - ein Wrapper über status_code<T> mit dem folgenden Konstruktor:


 errored_status_code(status_code<T>&& code) [[expects: code.failure() == true]] : code_(std::move(code)) {} 

Fehler


Der Standardausnahmetyp ( throws ohne Typ) sowie der Grundtyp der Ausnahmen, in die alle anderen umgewandelt werden (wie std::exception ), sind error . Es ist ungefähr so ​​definiert:


 using error = errored_status_code<intptr_t>; 

Das heißt, error ist ein solcher "Fehler" status_code , bei dem der Wert ( value ) in einen Zeiger gesetzt wird. Da der status_code_category Mechanismus das korrekte Löschen, status_code_category und Kopieren sicherstellt, kann theoretisch jede Datenstruktur error gespeichert error . In der Praxis ist dies eine der folgenden Optionen:


  1. Ganzzahlen (int)
  2. std::exception_handle , d. h. ein Zeiger auf eine ausgelöste dynamische Ausnahme
  3. status_code_ptr , d. status_code_ptr . unique_ptr zu einem beliebigen status_code<T> .

Das Problem ist, dass Fall 3 nicht geplant ist, um die Möglichkeit zu geben, error auf status_code<T> . Sie können nur die message() gepackten status_code<T> . Um den error Wert wieder zurückzubekommen, werfen Sie ihn als dynamische Ausnahme (!). Fangen Sie ihn dann ab und verpacken Sie ihn error . Im Allgemeinen ist Niall der Ansicht, dass nur Fehlercodes und Zeichenfolgenmeldungen error gespeichert error , was für jedes Programm ausreicht.


Um zwischen verschiedenen Arten von Fehlern zu unterscheiden, wird vorgeschlagen, den "virtuellen" Vergleichsoperator zu verwenden:


 try { open_file(name); } catch (std::error e) { if (e == filesystem_error::already_exists) { return; } else { throw my_exception("Unknown filesystem error, unable to continue"); } } 

Die Verwendung mehrerer Catch-Blöcke oder dynamic_cast zur Auswahl des Ausnahmetyps dynamic_cast fehl!


Interaktion mit dynamischen Ausnahmen


Eine Funktion kann eine der folgenden Spezifikationen haben:


  • noexcept : noexcept keine Ausnahmen aus
  • throws(E) : Wirft nur statische Ausnahmen
  • (nichts): löst nur dynamische Ausnahmen aus

throws implizieren noexcept . Wenn eine dynamische Ausnahme von einer "statischen" Funktion ausgelöst wird, wird sie in einen error . Wenn eine statische Ausnahme von einer "dynamischen" Funktion ausgelöst wird, wird sie in eine status_error Ausnahme eingeschlossen. Ein Beispiel:


 void foo() throws(arithmetic_errc) { throw erithmetic_errc::divide_by_zero; } void bar() throws { //  arithmetic_errc   intptr_t //     error foo(); } void baz() { // error    status_error bar(); } void qux() throws { // error    status_error baz(); } 

Ausnahmen in C ?!


Der Vorschlag sieht das Hinzufügen von Ausnahmen zu einem der zukünftigen C-Standards vor. Diese Ausnahmen sind ABI-kompatibel mit statischen C ++ - Ausnahmen. Bei einer ähnlichen Struktur wie std::expected<T, U> muss der Benutzer unabhängig deklarieren, obwohl Redundanz mithilfe von Makros entfernt werden kann. Die Syntax besteht aus (der Einfachheit halber nehmen wir dies an) den Schlüsselwörtern fehlgeschlagen, fehlgeschlagen, abfangen.


 int invert(int x) fails(float) { if (x != 0) return 1 / x; else return failure(2.0f); } struct expected_int_float { union { int value; float error; }; _Bool failed; }; void caller() { expected_int_float result = catch(invert(5)); if (result.failed) { print_error(result.error); return; } print_success(result.value); } 

Gleichzeitig ist es in C ++ auch möglich, fails Funktionen von C aus aufzurufen und in extern C Blöcken zu deklarieren. In C ++ gibt es also eine ganze Galaxie von Schlüsselwörtern für die Arbeit mit Ausnahmen:


  • throw() - in C ++ 20 entfernt
  • noexcept - Funktionsspezifizierer, die Funktion löst keine dynamischen Ausnahmen aus
  • noexcept(expression) - Funktionsspezifizierer, die Funktion löst keine bereitgestellten dynamischen Ausnahmen aus
  • noexcept(expression) - noexcept(expression) ein Ausdruck dynamische Ausnahmen?
  • throws(E) - Funktionsspezifizierer, die Funktion löst statische Ausnahmen aus
  • throws = throws(std::error)
  • fails(E) - Eine aus C importierte Funktion löst statische Ausnahmen aus

In C ++ haben sie also einen Wagen mit neuen Tools für die Fehlerbehandlung eingeführt (oder vielmehr geliefert). Als nächstes stellt sich eine logische Frage:


Wann was verwenden?


Allgemeine Richtung


Fehler sind in mehrere Ebenen unterteilt:


  • Programmiererfehler. Mit Verträgen verarbeitet. Sie führen zur Erfassung von Protokollen und zur Beendigung des Programms gemäß dem Konzept des Fail-Fast . Beispiele: Nullzeiger (wenn dieser ungültig ist); Division durch Null; Speicherzuordnungsfehler, die vom Programmierer nicht vorhergesehen wurden.
  • Schwerwiegende Fehler des Programmierers. Wird millionenfach seltener als eine normale Rückgabe einer Funktion ausgegeben, wodurch die Verwendung dynamischer Ausnahmen für sie gerechtfertigt ist. In solchen Fällen müssen Sie normalerweise das gesamte Subsystem des Programms neu starten oder bei der Ausführung des Vorgangs einen Fehler angeben. Beispiele: plötzlich verlorene Verbindung zur Datenbank; Vom Programmierer bereitgestellte Speicherzuordnungsfehler.
  • Behebbare Fehler, wenn etwas die Funktion daran gehindert hat, ihre Aufgabe zu erledigen, die aufrufende Funktion jedoch möglicherweise weiß, was damit zu tun ist. Wird durch statische Ausnahmen behandelt. Beispiele: Arbeiten mit dem Dateisystem; andere Eingabe- / Ausgabefehler (E / A); Falsche Benutzerdaten vector::at() .
  • Die Funktion hat ihre Aufgabe erfolgreich abgeschlossen, wenn auch mit einem unerwarteten Ergebnis. std::optional , std::expected , std::variant . Beispiele: stoi() ; vector::find() ; map::insert .

In der Standardbibliothek ist es am zuverlässigsten, die Verwendung dynamischer Ausnahmen vollständig aufzugeben, um die Kompilierung "ohne Ausnahmen" legal zu machen.


errno


Funktionen, die errno um schnell und einfach mit C- und C ++ - Fehlercodes zu arbeiten, sollten durch throws(std::errc) fails(int) bzw. throws(std::errc) ersetzt werden. Für einige Zeit werden die alte und die neue Version der Funktionen der Standardbibliothek nebeneinander existieren, dann wird die alte für veraltet erklärt.


Nicht genügend Speicher


Speicherzuordnungsfehler werden vom globalen Hook new_handler , der:


  1. Beseitigen Sie Speichermangel und setzen Sie die Ausführung fort
  2. Eine Ausnahme auslösen
  3. Absturzprogramm

Jetzt wird standardmäßig std::bad_alloc ausgelöst. Es wird empfohlen, standardmäßig std::terminate() aufzurufen. Wenn Sie das alte Verhalten benötigen, ersetzen Sie den Handler durch den am Anfang von main() .


Alle vorhandenen Funktionen der Standardbibliothek werden nicht mehr noexcept und stürzen das Programm ab, wenn std::bad_alloc . Gleichzeitig werden neue APIs wie vector::try_push_back hinzugefügt, die Speicherzuordnungsfehler zulassen.


logic_error


Ausnahmen std::logic_error , std::domain_error , std::invalid_argument , std::length_error , std::out_of_range , std::future_error melden einen Verstoß gegen eine Funktionsvoraussetzung. Das neue Fehlermodell sollte stattdessen Verträge verwenden. Die aufgeführten Arten von Ausnahmen werden nicht veraltet sein, aber fast alle Fälle ihrer Verwendung in der Standardbibliothek werden durch [[expects: …]] .


Aktueller Angebotsstatus


Der Vorschlag befindet sich derzeit in einem Entwurfszustand. Es hat sich bereits sehr verändert und kann sich noch sehr verändern. Einige Entwicklungen konnten nicht veröffentlicht werden, daher ist die vorgeschlagene API <system_error2> nicht vollständig relevant.


Der Vorschlag ist in 3 Dokumenten beschrieben:


  1. P709 - Originaldokument aus dem Wappen von Sutter
  2. P1095 - Bestimmte Ausnahmen in Niall Douglas Vision, einige Momente geändert, Kompatibilität mit C-Sprache hinzugefügt
  3. P1028 - API aus der Testimplementierung von std::error

Derzeit gibt es keinen Compiler, der statische Ausnahmen unterstützt. Dementsprechend ist es noch nicht möglich, ihre Benchmarks zu erstellen.


C++23. , , , C++26, , , .


Fazit


, , . , . .


, ^^

Source: https://habr.com/ru/post/de430690/


All Articles