c++ - werror - Wann sollte ich wirklich noexcept verwenden?




warning dynamic exception specifications are deprecated in c++ 11[-< deprecated>] (6)

  1. Es gibt viele Beispiele für Funktionen, von denen ich weiß, dass sie niemals geworfen werden, für die der Compiler dies jedoch nicht selbst bestimmen kann. Sollte ich in all diesen Fällen noexcept an die Funktionsdeklaration anhängen?

noexcept ist schwierig, da es Teil der Funktionsschnittstelle ist. Besonders wenn Sie eine Bibliothek schreiben, kann Ihr Client-Code von der noexcept Eigenschaft abhängen. Es kann schwierig sein, es später zu ändern, da Sie möglicherweise vorhandenen Code beschädigen. Das ist weniger wichtig, wenn Sie Code implementieren, der nur von Ihrer Anwendung verwendet wird.

Wenn Sie eine Funktion haben, die nicht werfen kann, fragen Sie sich, ob es nicht bleiben noexcept oder würde das zukünftige Implementierungen einschränken? Beispielsweise möchten Sie möglicherweise eine Fehlerüberprüfung für unzulässige Argumente einführen, indem Sie Ausnahmen (z. B. für Komponententests) auslösen, oder Sie sind möglicherweise von anderem Bibliothekscode abhängig, der seine Ausnahmebestimmung ändern könnte. In diesem Fall ist es sicherer, konservativ zu sein und noexcept .

Auf der anderen Seite, wenn Sie sicher sind, dass die Funktion nie werfen sollte und es korrekt ist, dass es Teil der Spezifikation ist, sollten Sie es noexcept deklarieren. noexcept Sie jedoch, dass der Compiler keine Verletzungen von noexcept erkennen noexcept wenn sich Ihre Implementierung ändert.

  1. Für welche Situationen sollte ich bei der Verwendung von noexcept vorsichtiger sein und für welche Situationen kann ich mit dem implizierten noexcept (false) davonkommen?

Es gibt vier Klassen von Funktionen, auf die Sie sich konzentrieren sollten, da sie wahrscheinlich die größten Auswirkungen haben werden:

  1. Operationen verschieben (Zuweisungsoperator verschieben und Konstruktoren verschieben)
  2. Tauschvorgänge
  3. Speicherfreigabe (Operator delete, operator delete [])
  4. Destruktoren (obwohl diese implizit nicht noexcept(true) sei denn, Sie machen sie noexcept(false) )

Diese Funktionen sollten im Allgemeinen keine noexcept und es ist sehr wahrscheinlich, dass Bibliotheksimplementierungen die noexcept Eigenschaft verwenden können. Zum Beispiel kann std::vector nicht-würfelnde Verschiebungsoperationen verwenden, ohne starke Ausnahmegarantien zu opfern. Andernfalls muss es auf die Kopierelemente zurückgreifen (wie in C ++ 98).

Diese Art der Optimierung ist auf der algorithmischen Ebene und nicht auf Compiler-Optimierungen angewiesen. Dies kann erhebliche Auswirkungen haben, insbesondere wenn die Elemente teuer zu kopieren sind.

  1. Wann kann ich realistischerweise erwarten, dass eine Leistungsverbesserung nach der Verwendung von noexcept beobachtet wird? Geben Sie insbesondere ein Beispiel für Code an, für den ein C ++ - Compiler nach dem Hinzufügen von noexcept einen besseren Maschinencode generieren kann.

Der Vorteil von noexcept gegen keine Ausnahmespezifikation oder throw() ist, dass der Standard den Compilern mehr Freiheit beim Abwickeln von Stacks ermöglicht. Selbst im Fall von throw() muss der Compiler den Stapel vollständig abwickeln (und zwar in der genau umgekehrten Reihenfolge der Objektkonstruktionen).

Im noexcept Fall ist dies jedoch nicht erforderlich. Es gibt keine Anforderung, dass der Stapel abgewickelt werden muss (aber der Compiler darf dies weiterhin tun). Diese Freiheit ermöglicht eine weitere Code-Optimierung, da sie den Aufwand reduziert, den Stack immer wieder abwickeln zu können.

Die damit verbundene Frage nach der Nichtabnahme, der Stapelabwickelung und der Leistung geht näher auf den Overhead ein, wenn das Abwickeln des Stapels erforderlich ist.

Ich empfehle auch Scott Meyers Buch "Effective Modern C ++", "Item 14: Declare Funktionen noexcept, wenn sie keine Ausnahmen ausgeben" für weitere Lektüre.

Das noexcept Schlüsselwort kann auf viele Funktionssignaturen angemessen angewendet werden, aber ich bin unsicher, wann ich es in der Praxis in Erwägung ziehen sollte. Basierend auf dem, was ich bisher gelesen habe, scheint die letzte Ergänzung von noexcept einige wichtige Probleme anzugehen, die entstehen, wenn Move-Konstruktoren werfen. Ich bin jedoch immer noch nicht in der Lage, befriedigende Antworten auf einige praktische Fragen zu geben, die mich dazu veranlasst haben, mehr über noexcept zu lesen.

  1. Es gibt viele Beispiele für Funktionen, von denen ich weiß, dass sie niemals geworfen werden, für die der Compiler dies jedoch nicht selbst bestimmen kann. Sollte ich in all diesen Fällen noexcept an die Funktionsdeklaration anhängen ?

    noexcept Gedanken zu machen, ob ich nach jeder Funktionsdeklaration keine noexcept anhängen noexcept oder nicht, würde die Produktivität des Programmierers stark reduzieren (und ehrlich gesagt wäre es eine noexcept ). Für welche Situationen sollte ich bei der Verwendung von noexcept vorsichtiger sein und für welche Situationen kann ich mit dem implizierten noexcept(false) ?

  2. Wann kann ich realistischerweise erwarten, dass eine Leistungsverbesserung nach der Verwendung von noexcept ? Geben Sie insbesondere ein Beispiel für Code an, für den ein C ++ - Compiler nach dem Hinzufügen von noexcept einen besseren Maschinencode generieren noexcept .

    Persönlich interessiere ich mich für noexcept wegen der erhöhten Freiheit, die dem Compiler geboten wird, um gewisse Arten von Optimierungen sicher anzuwenden. Nutzen moderne Compiler die Vorteile von noexcept auf diese Weise? Wenn nicht, kann ich erwarten, dass einige von ihnen das in naher Zukunft tun?


Es gibt viele Beispiele für Funktionen, von denen ich weiß, dass sie niemals geworfen werden, für die der Compiler dies jedoch nicht selbst bestimmen kann. Sollte ich in all diesen Fällen noexcept an die Funktionsdeklaration anhängen?

Wenn Sie sagen: "Ich weiß, [sie] werden nie werfen", meinen Sie, indem Sie die Implementierung der Funktion untersuchen, die Sie wissen, dass die Funktion nicht werfen wird. Ich denke, dieser Ansatz ist von innen nach außen.

Es ist besser zu überlegen, ob eine Funktion Ausnahmen auslösen kann, um Teil des Entwurfs der Funktion zu sein: so wichtig wie die Argumentliste und ob eine Methode ein Mutator ist (... const ). Zu erklären, dass "diese Funktion niemals Ausnahmen auslöst", ist eine Einschränkung für die Implementierung. Wenn man es auslässt, heißt das nicht, dass die Funktion Ausnahmen auslösen könnte; Das bedeutet, dass die aktuelle Version der Funktion und alle zukünftigen Versionen Ausnahmen auslösen können. Es ist eine Einschränkung, die die Implementierung erschwert. Bei einigen Methoden muss die Einschränkung jedoch praktisch nützlich sein. Am wichtigsten ist, dass sie von Destruktoren aufgerufen werden können, aber auch für die Implementierung von "Roll-Back" -Code in Methoden, die die starke Ausnahme garantieren.


Dies macht tatsächlich einen (potentiell) großen Unterschied zum Optimierer im Compiler. Compiler haben dieses Feature seit Jahren über die leere throw () - Anweisung nach einer Funktionsdefinition sowie proprietäre Erweiterungen. Ich kann Ihnen versichern, dass moderne Compiler dieses Wissen nutzen, um besseren Code zu generieren.

Fast jede Optimierung im Compiler verwendet ein sogenanntes "Flow Graph" einer Funktion, um zu begründen, was legal ist. Ein Flussdiagramm besteht aus Blöcken, die allgemein als "Blöcke" der Funktion bezeichnet werden (Codebereiche, die einen einzelnen Eingang und einen einzelnen Ausgang haben) und Kanten zwischen den Blöcken, um anzuzeigen, wohin der Fluss springen kann. Noexcept ändert das Flussdiagramm.

Sie haben nach einem bestimmten Beispiel gefragt. Betrachten Sie diesen Code:

void foo(int x) {
    try {
        bar();
        x = 5;
        // other stuff which doesn't modify x, but might throw
    } catch(...) {
        // don't modify x
    }

    baz(x); // or other statement using x
}

Das Ablaufdiagramm für diese Funktion ist unterschiedlich, wenn der Balken mit "noexcept" beschriftet ist (es gibt keine Möglichkeit für die Ausführung, zwischen dem Ende des Balkens und der Fanganweisung zu springen). Wenn er als noexcept bezeichnet wird, ist der Compiler sicher, dass der Wert von x während der baz-Funktion 5 ist - der Block x = 5 soll den Block baz (x) ohne die Kante von bar () zur Fanganweisung "dominieren". Es kann dann etwas tun, das als "konstante Ausbreitung" bezeichnet wird, um effizienteren Code zu erzeugen. Hier, wenn baz inline ist, können die Anweisungen, die x verwenden, auch Konstanten enthalten und dann, was früher eine Laufzeitbewertung war, kann in eine Kompilierzeitauswertung umgewandelt werden usw.

Wie auch immer, kurze Antwort: noexcept lässt den Compiler ein tighteres Flow-Diagramm generieren, und das Flow-Graph wird verwendet, um über alle Arten gängiger Compiler-Optimierungen nachzudenken. Für einen Compiler sind Benutzeranmerkungen dieser Art genial. Der Compiler wird versuchen, dieses Zeug herauszufinden, aber es kann normalerweise nicht (die betreffende Funktion könnte in einer anderen Objektdatei sein, die für den Compiler nicht sichtbar ist oder transitiv eine Funktion verwendet, die nicht sichtbar ist), oder wenn es welche gibt triviale Ausnahme, die ausgelöst werden kann und die Ihnen nicht einmal bewusst ist, so dass sie nicht implizit als noexcept gekennzeichnet werden kann (das Zuweisen von Speicher könnte beispielsweise bad_alloc auslösen).


Ich denke, es ist zu früh, um eine "Best Practices" -Antwort dafür zu geben, da es nicht genug Zeit gegeben hat, um es in der Praxis anzuwenden. Wenn dies nach dem Herauskommen von Wurfspezifizierern gefragt würde, wären die Antworten sehr unterschiedlich.

noexcept Gedanken zu machen, ob ich nach jeder Funktionsdeklaration noexcept anhängen noexcept oder nicht, würde die Programmiererproduktivität stark reduzieren (und ehrlich gesagt, wäre ein Schmerz).

Na dann benutze es wenn es offensichtlich ist, dass die Funktion niemals werfen wird.

Wann kann ich realistischerweise erwarten, dass eine Leistungsverbesserung nach der Verwendung von noexcept ? [...] Persönlich interessiere ich mich für noexcept weil dem Compiler erhöhte Freiheit noexcept , bestimmte Arten von Optimierungen sicher anzuwenden.

Es scheint, als ob die größten Optimierungsgewinne von Benutzeroptimierungen noexcept , nicht von Kompilierern, da es möglich ist, noexcept überprüfen und zu überlasten. Die meisten Compiler folgen der Exception-Methode "No-Penalty-Wenn-Du-Wirf-Throw", so dass ich bezweifle, dass sich dies auf der Maschinencodeebene des Codes ändern wird (obwohl die Binärgröße möglicherweise durch Entfernen der Verarbeitung reduziert wird) Code.

Die Verwendung von noexcept in den großen 4 (Konstruktoren, Zuweisung, nicht Destruktoren, da sie bereits nicht noexcept ) führt wahrscheinlich zu den besten Verbesserungen, da noexcept im Vorlagencode, wie in noexcept , üblich sind. Zum Beispiel wird std::vector die Bewegung Ihrer Klasse nicht verwenden, es sei denn, sie ist als noexcept markiert (oder der Compiler kann sie anderweitig ableiten).


Wie ich immer wieder wiederhole: Semantik zuerst .

Das Hinzufügen von noexcept , noexcept(true) und noexcept(false) geht in erster Linie um Semantik. Es bedingt nur eine Reihe von möglichen Optimierungen.

Als Programmierer, der Code liest, ist das Vorhandensein von noexcept dem von const ähnlich: es hilft mir besser zu verstehen, was passieren könnte oder nicht. Daher lohnt es sich, etwas darüber nachzudenken, ob Sie wissen, ob die Funktion ausgelöst wird oder nicht. Zur Erinnerung, jede Art von dynamischer Speicherzuweisung kann werfen.

Okay, jetzt zu den möglichen Optimierungen.

Die offensichtlichsten Optimierungen werden tatsächlich in den Bibliotheken durchgeführt. C ++ 11 bietet eine Reihe von Merkmalen, die es ermöglichen, zu wissen, ob eine Funktion nicht noexcept oder nicht, und die Standardbibliotheksimplementierung selbst verwendet diese Merkmale, um noexcept keine noexcept für die von ihnen manipulierten benutzerdefinierten Objekte zu bevorzugen. Wie zum Beispiel die Semantik .

Der Compiler kann (vielleicht) nur etwas Fett von den Ausnahmebehandlungsdaten nehmen, weil er die Tatsache berücksichtigen muss, dass Sie gelogen haben. Wenn eine mit noexcept gekennzeichnete Funktion noexcept wird, wird std::terminate aufgerufen.

Diese Semantik wurde aus zwei Gründen gewählt:

  • sofort von noexcept profitieren, auch wenn Abhängigkeiten es nicht bereits nutzen (Abwärtskompatibilität)
  • noexcept der Spezifikation von noexcept beim Aufrufen von Funktionen, die theoretisch werfen können, aber für die gegebenen Argumente nicht erwartet werden

noexcept kann die Leistung einiger Operationen drastisch verbessern. Dies geschieht nicht auf der Ebene des Generierens von Maschinencode durch den Compiler, sondern durch Auswählen des effektivsten Algorithmus: Wie andere erwähnt haben, tun Sie diese Auswahl unter Verwendung der Funktion std::move_if_noexcept . Zum Beispiel muss das Wachstum von std::vector (z. B. wenn wir reserve nennen) eine starke Ausnahmesicherheitsgarantie bieten. Wenn es weiß, dass der Move-Konstruktor von T nicht wirft, kann er einfach jedes Element verschieben. Ansonsten muss es alle T s kopieren. Dies wurde in diesem Beitrag ausführlich beschrieben.





noexcept