it-swarm.com.de

Wann sollte ich noexcept wirklich verwenden?

Das Schlüsselwort noexcept kann auf viele Funktionssignaturen angewendet werden, aber ich bin mir nicht sicher, wann ich es in der Praxis verwenden soll. Basierend auf dem, was ich bisher gelesen habe, scheint die Last-Minute-Hinzufügung von noexcept einige wichtige Probleme anzugehen, die beim Werfen von Move-Konstruktoren auftreten. Es ist mir jedoch immer noch nicht möglich, zufriedenstellende Antworten auf einige praktische Fragen zu geben, die mich überhaupt erst dazu veranlassten, mehr über noexcept zu lesen.

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

    Überlegen zu müssen, ob ich noexcept nach jeder Funktionsdeklaration anhängen muss oder nicht, würde die Produktivität des Programmierers erheblich verringern (und wäre ehrlich gesagt ein Ärgernis). In welchen Situationen sollte ich bei der Verwendung von noexcept vorsichtiger sein und in welchen Situationen kann ich mit der implizierten noexcept(false) durchkommen?

  2. Wann kann ich nach der Verwendung von noexcept realistisch mit einer Leistungsverbesserung rechnen? Geben Sie insbesondere ein Beispiel für Code an, für den ein C++ - Compiler nach dem Hinzufügen von noexcept besseren Maschinencode generieren kann.

    Persönlich kümmere ich mich um noexcept, da der Compiler mehr Freiheit hat, bestimmte Arten von Optimierungen sicher anzuwenden. Nutzen moderne Compiler noexcept auf diese Weise? Wenn nicht, kann ich davon ausgehen, dass einige dies in naher Zukunft tun?

452
void-pointer

Ich denke, es ist zu früh, um eine Antwort auf "Best Practices" zu geben, da nicht genügend Zeit vorhanden ist, um sie in der Praxis anzuwenden. Wenn dies direkt nach dem Erscheinen nach den Wurfspezifizierern gefragt würde, wären die Antworten ganz anders als jetzt.

Überlegen zu müssen, ob ich noexcept nach jeder Funktionsdeklaration anhängen muss oder nicht, würde die Produktivität des Programmierers erheblich verringern (und wäre ehrlich gesagt ein Schmerz).

Dann benutze es, wenn es offensichtlich ist, dass die Funktion niemals auslöst.

Wann kann ich nach der Verwendung von noexcept realistisch mit einer Leistungsverbesserung rechnen? [...] Ich persönlich interessiere mich für noexcept, da der Compiler mehr Freiheit hat, bestimmte Arten von Optimierungen sicher anzuwenden.

Es scheint, dass die größten Optimierungsgewinne durch Benutzeroptimierungen erzielt werden, nicht durch Compiler-Optimierungen, da die Möglichkeit besteht, noexcept zu überprüfen und zu überladen. Die meisten Compiler verwenden eine Ausnahmebehandlungsmethode, bei der keine Strafe verhängt wird. Daher bezweifle ich, dass sich dadurch viel (oder nichts) auf der Maschinencode-Ebene Ihres Codes ändert, obwohl die Binärgröße möglicherweise durch Entfernen der Behandlung verringert wird Code.

Die Verwendung von noexcept in den Big 4 (Konstruktoren, Zuweisung, nicht Destruktoren, da sie bereits noexcept sind) führt wahrscheinlich zu den besten Verbesserungen, da noexcept Prüfungen in der Vorlage 'häufig' sind Code wie in Standardcontainern. Zum Beispiel, std::vector verwendet den Zug Ihrer Klasse nur, wenn er mit noexcept markiert ist (oder der Compiler kann ihn auf andere Weise ableiten).

167
Pubby

Wie ich in diesen Tagen immer wieder wiederhole: Semantik zuerst.

Beim Hinzufügen von noexcept, noexcept(true) und noexcept(false) geht es in erster Linie um die Semantik. Es werden nur gelegentlich eine Reihe möglicher Optimierungen vorgenommen.

Als Programmierer, der Code liest, ist das Vorhandensein von noexcept dem von const ähnlich: Es hilft mir, besser zu verstehen, was passieren kann oder nicht. Daher lohnt es sich, sich einige Zeit zu überlegen, ob Sie wissen, ob die Funktion ausgelöst wird oder nicht. Zur Erinnerung, jede Art von dynamischer Speicherzuweisung kann ausgelöst werden.


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, mit denen festgestellt werden kann, ob eine Funktion noexcept ist oder nicht, und die Implementierung der Standardbibliothek selbst verwendet diese Merkmale, um noexcept -Operationen für benutzerdefinierte Objekte zu begünstigen Sie manipulieren, wenn möglich. Wie verschieben Semantik .

Der Compiler kann (möglicherweise) nur ein wenig Fett von den Ausnahmebehandlungsdaten entfernen, da er die Tatsache berücksichtigen muss , dass Sie möglicherweise gelogen haben . Wenn eine mit noexcept markierte Funktion ausgelöst wird, dann std::terminate wird genannt.

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

  • sofort von noexcept profitieren, auch wenn Abhängigkeiten es noch nicht verwenden (Abwärtskompatibilität)
  • zulassen der Angabe von noexcept beim Aufrufen von Funktionen, die theoretisch ausgelöst werden können, aber für die angegebenen Argumente nicht erwartet werden
123
Matthieu M.

Dies macht tatsächlich einen (potenziellen) großen Unterschied für den Optimierer im Compiler. Compiler verfügen über diese Funktion bereits seit Jahren über die Anweisung empty throw () nach einer Funktionsdefinition sowie über Erweiterungen der Eigenschaften. Ich kann Ihnen versichern, dass moderne Compiler dieses Wissen nutzen, um besseren Code zu generieren.

Nahezu jede Optimierung im Compiler verwendet ein sogenanntes "Flussdiagramm" einer Funktion, um zu überlegen, was zulässig ist. Ein Flussdiagramm besteht aus sogenannten "Blöcken" der Funktion (Codebereiche mit einem einzelnen Eingang und einem einzelnen Ausgang) 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 Flussdiagramm für diese Funktion unterscheidet sich, wenn bar mit noexcept gekennzeichnet ist (es besteht keine Möglichkeit, zwischen dem Ende von bar und der catch-Anweisung zu springen). Wenn der Compiler als noexcept bezeichnet ist, ist der Wert von x während der Baz-Funktion sicher - der Block x = 5 soll den Baz (x) -Block ohne die Kante von bar() zur catch-Anweisung. Es kann dann etwas tun, das als "konstante Weitergabe" bezeichnet wird, um effizienteren Code zu generieren. Wenn baz inline gesetzt ist, können die Anweisungen, die x verwenden, auch Konstanten enthalten. Was früher eine Laufzeitauswertung war, kann dann in eine Auswertung zur Kompilierungszeit usw. umgewandelt werden.

Wie auch immer, kurze Antwort: noexcept lässt den Compiler ein engeres Flussdiagramm generieren, und das Flussdiagramm wird verwendet, um über alle Arten von allgemeinen Compileroptimierungen nachzudenken. Für einen Compiler sind Benutzeranmerkungen dieser Art fantastisch. Der Compiler wird versuchen, dies herauszufinden, kann es aber normalerweise nicht (die betreffende Funktion befindet sich möglicherweise in einer anderen Objektdatei, die für den Compiler nicht sichtbar ist, oder verwendet transitiv eine Funktion, die nicht sichtbar ist), oder wenn dies der Fall ist, gibt es einige triviale Ausnahme, die möglicherweise ausgelöst wird und die Ihnen nicht einmal bewusst ist, sodass sie nicht implizit als noexcept gekennzeichnet werden kann (das Zuweisen von Speicher kann beispielsweise bad_alloc auslösen).

73
Terry Mahaffey

noexcept kann die Leistung einiger Vorgänge erheblich verbessern. Dies geschieht nicht auf der Ebene der Generierung von Maschinencode durch den Compiler, sondern durch Auswahl des effektivsten Algorithmus: Wie bereits erwähnt, können Sie diese Auswahl mit der Funktion std::move_if_noexcept. Zum Beispiel das Wachstum von std::vector (z. B. wenn wir reserve aufrufen) muss eine starke Ausnahmesicherheitsgarantie bieten. Wenn es weiß, dass der move-Konstruktor von T nicht ausgelöst wird, kann er einfach jedes Element verschieben. Andernfalls müssen alle Ts kopiert werden. Dies wurde ausführlich in dieser Beitrag beschrieben.

50
Andrzej

Wann kann ich eine Leistungsverbesserung nach Verwendung von noexcept realistisch ausschließen? Geben Sie insbesondere ein Beispiel für Code an, für den ein C++ - Compiler nach dem Hinzufügen von noexcept besseren Maschinencode generieren kann.

Niemals? Ist nie eine Zeit? Noch nie.

noexcept ist für Compiler Leistungsoptimierungen in der gleichen Weise wie const für Leistungsoptimierungen des Compilers. Das heißt, fast nie.

noexcept wird hauptsächlich verwendet, damit "Sie" beim Kompilieren erkennen können, ob eine Funktion eine Ausnahme auslösen kann. Denken Sie daran: Die meisten Compiler geben keinen speziellen Code für Ausnahmen aus, es sei denn, er löst tatsächlich etwas aus. Mit noexcept müssen Sie dem Compiler also nicht nur Hinweise zur Optimierung einer Funktion geben, sondern auch yo Hinweise zur Verwendung einer Funktion.

Vorlagen wie move_if_noexcept erkennt, ob der Verschiebungskonstruktor mit noexcept definiert ist und gibt ein const& anstelle einer && vom Typ, wenn dies nicht der Fall ist. Es ist eine Art zu sagen, sich zu bewegen, wenn dies sehr sicher ist.

Im Allgemeinen sollten Sie noexcept verwenden, wenn Sie glauben, dass dies tatsächlich nützlich ist. Einige Codes verwenden andere Pfade, wenn is_nothrow_constructible ist für diesen Typ wahr. Wenn Sie dazu Code verwenden, können Sie die entsprechenden Konstruktoren noexcept verwenden.

Kurz gesagt: Verwenden Sie es für Move-Konstruktoren und ähnliche Konstrukte, aber Sie haben nicht das Gefühl, verrückt werden zu müssen.

30
Nicol Bolas
  1. Es gibt viele Beispiele für Funktionen, von denen ich weiß, dass sie niemals ausgelöst werden, für die der Compiler dies jedoch nicht allein bestimmen kann. Sollte ich in all diesen Fällen keine Ausnahme von der Funktionsdeklaration machen?

noexcept ist schwierig, da es Teil der Funktionsschnittstelle ist. Insbesondere wenn Sie eine Bibliothek schreiben, kann Ihr Client-Code von der Eigenschaft noexcept abhängen. Eine spätere Änderung kann schwierig sein, da der vorhandene Code möglicherweise beschädigt wird. Dies ist möglicherweise weniger problematisch, wenn Sie Code implementieren, der nur von Ihrer Anwendung verwendet wird.

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

Wenn Sie dagegen sicher sind, dass die Funktion niemals ausgelöst wird und es korrekt ist, dass sie Teil der Spezifikation ist, sollten Sie sie als noexcept deklarieren. Beachten Sie jedoch, dass der Compiler Verstöße gegen noexcept nicht erkennen kann, wenn sich Ihre Implementierung ändert.

  1. In welchen Situationen sollte ich bei der Verwendung von noexcept vorsichtiger sein, und in welchen Situationen kann ich mit dem implizierten noexcept (false) davonkommen?

Es gibt vier Funktionsklassen, auf die Sie sich konzentrieren sollten, da sie wahrscheinlich den größten Einfluss haben werden:

  1. verschiebeoperationen (Verschiebezuweisungsoperator und Verschiebekonstruktoren)
  2. swap-Operationen
  3. speicherfreigaben (Operator löschen, Operator löschen [])
  4. destruktoren (obwohl diese implizit noexcept(true) sind, es sei denn, Sie machen sie noexcept(false))

Diese Funktionen sollten im Allgemeinen noexcept sein, und es ist sehr wahrscheinlich, dass Bibliotheksimplementierungen die Eigenschaft noexcept verwenden können. Zum Beispiel kann std::vector Nicht auslösende Bewegungsoperationen verwenden, ohne starke Ausnahmegarantien zu opfern. Andernfalls muss auf das Kopieren von Elementen zurückgegriffen werden (wie in C++ 98).

Diese Art der Optimierung erfolgt auf algorithmischer Ebene und basiert nicht auf Compiler-Optimierungen. Dies kann erhebliche Auswirkungen haben, insbesondere wenn das Kopieren der Elemente teuer ist.

  1. Wann kann ich nach der Verwendung von noexcept realistisch mit einer Leistungsverbesserung rechnen? Geben Sie insbesondere ein Beispiel für Code an, für den ein C++ - Compiler nach dem Hinzufügen von noexcept besseren Maschinencode generieren kann.

Der Vorteil von noexcept gegenüber keiner Ausnahmespezifikation oder throw() ist, dass der Standard den Compilern mehr Freiheit beim Abwickeln von Stapeln bietet. Selbst in dem Fall throw() muss der Compiler den Stapel vollständig abwickeln (und dies in der genau umgekehrten Reihenfolge der Objektkonstruktionen).

Im Fall noexcept ist dies nicht erforderlich. Es ist nicht erforderlich, dass der Stack abgewickelt werden muss (der Compiler darf dies jedoch weiterhin tun). Diese Freiheit ermöglicht eine weitere Codeoptimierung, da der Aufwand verringert wird, den Stapel immer abwickeln zu können.

Die zugehörige Frage zu noexcept, Stack Unwinding und Performance befasst sich ausführlicher mit dem Overhead, wenn Stack Unwinding erforderlich ist.

Ich empfehle auch Scott Meyers Buch "Effective Modern C++", "Punkt 14: Deklarieren von Funktionen, außer wenn sie keine Ausnahmen ausgeben", zum weiteren Lesen.

19
Philipp Claßen

In Bjarnes Worten:

Wenn die Beendigung eine akzeptable Antwort ist, wird dies durch eine nicht abgefangene Ausnahme erreicht, da daraus ein Aufruf von terminate () wird (§ 13.5.2.5). Auch ein noexcept -Spezifizierer (§13.5.1.1) kann diesen Wunsch explizit machen.

Erfolgreiche fehlertolerante Systeme sind mehrstufig. Jede Ebene bewältigt so viele Fehler wie möglich, ohne zu verzerrt zu werden, und überlässt den Rest höheren Ebenen. Ausnahmen unterstützen diese Ansicht. Darüber hinaus unterstützt terminate() diese Ansicht, indem es ein Escape bereitstellt, wenn der Ausnahmebehandlungsmechanismus selbst beschädigt ist oder unvollständig verwendet wurde Damit bleiben Ausnahmen unberührt. In ähnlicher Weise bietet noexcept eine einfache Möglichkeit, Fehler zu beheben, bei denen der Versuch einer Wiederherstellung unmöglich erscheint.

 double compute(double x) noexcept;   {       
     string s = "Courtney and Anya"; 
     vector<double> tmp(10);      
     // ...   
 }

Der Vektorkonstruktor kann möglicherweise keinen Speicher für seine zehn Double abrufen und einen std::bad_alloc Auslösen. In diesem Fall wird das Programm beendet. Sie wird unbedingt durch Aufrufen von std::terminate() (§30.4.1.3) beendet. Es ruft keine Destruktoren von aufrufenden Funktionen auf. Es ist implementierungsdefiniert, ob Destruktoren aus Bereichen zwischen throw und noexcept (z. B. für s in compute ()) aufgerufen werden. Das Programm wird gerade beendet, daher sollten wir uns sowieso nicht auf ein Objekt verlassen. Durch Hinzufügen eines noexcept -Spezifizierers geben wir an, dass unser Code nicht für einen Throw geschrieben wurde.

17
Saurav Sahu

Es gibt viele Beispiele für Funktionen, von denen ich weiß, dass sie niemals ausgelöst werden, für die der Compiler dies jedoch nicht allein bestimmen kann. Sollte ich in all diesen Fällen keine Ausnahme von der Funktionsdeklaration machen?

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

Es ist besser zu überlegen, ob eine Funktion Ausnahmen auslöst, um Teil des design der Funktion zu sein: so wichtig wie die Argumentliste und ob eine Methode ein Mutator ist (... const). Die Angabe, dass "diese Funktion niemals Ausnahmen auslöst", ist eine Einschränkung für die Implementierung. Wenn Sie es weglassen, kann die Funktion keine Ausnahmen auslösen. Dies bedeutet, dass die aktuelle Version der Funktion nd alle zukünftigen Versionen möglicherweise Ausnahmen auslösen. Dies ist eine Einschränkung, die die Implementierung erschwert. Einige Methoden müssen jedoch die Einschränkung haben, praktisch nützlich zu sein. Am wichtigsten ist jedoch, dass sie von Destruktoren aufgerufen werden können, aber auch für die Implementierung von "Rollback" -Code in Methoden, die eine starke Ausnahmegarantie bieten.

15
Raedwald