Wann noexcept?



  • Gibt es mittlerweile eigentlich eine gute Zusammenstellung von Faustregeln, wann noexcept Sinn macht und wann man darauf verzichten kann?

    Theoretisch kann ich die noexcept-Expressions ja beliebig komplex machen, bis zu dem Punkt, wo ich mehr oder weniger die ganze Funktion in dem Ausruck nachbilde: noexcept(noexcept(T{}) && noexcept(f(std::declval<T>()) && ...). Das ist natürlich nicht Sinn der Sache und führt zu gruseligen Interfaces (und sollte in dieser Detailstufe auch eher ein Compiler-Job sein).

    Bisher ist mir lediglich ein Typ von Funktionen aufgefallen, bei denen ein fehlendes noexcept nachweislich negative Auswirklungen hat: Move-Konstruktoren und Assignment. Implementierungen von std::vector z.B. arbeiten aufgrund ihrer Strong Exception Guarantee intern mit std::move_if_noexcept o.ä., wenn Elemente verschoben werden müssen. Wenn die Move-Operation nicht noexcept deklariert ist, machen sie stattdessen eine Kopie, was unter Umständen unnötig teuer sein kann.

    Das wäre z.B. so eine Faustregel: Move-Konstruktoren/Assignment sollten noexcept sein, es sei denn es ist absolut unvermeidbar.

    Kennt jemand noch andere Beispiele oder gibt es irgendwo schon eine gute "Best Practices"-Zusammenstellung? Ich würde am liebsten noexcept ausschlieslich dort einsetzen, wo es nachweislich von Vorteil ist, da ich meinen Code gerne so simpel wie möglich halte (nichts gegen noexcept, aber es ist schon ein zusäzlicher Komplexitäts-Layer mit nicht immer klarem Nutzen).


  • Mod

    Es gibt zwei Wege, auf die noexcept-ness das Program beeinflussen kann.

    1. Der Compiler sieht, dass ein gewisser Ausdruck keine Exception werfen kann (weil während der Auswertung sonst terminate aufgerufen wird). Dadurch kann der Compiler potenziell besseren Code generieren, weil er nicht sicherstellen muss, dass die Funktion an diesem Punkt unwindable ist.
    2. Eine Bibliothek prüft, ob ein gewisser Ausdruck mit einem Template-Parameter noexcept ist, und verhält sich entsprechend unterschiedlich.

    Punkt 2. gilt prinzipiell fuer special member functions, von denen Du ja die relevantesten bereits erwähnt hast.

    Punkt 1. ist fuer Funktionen, die geinlined werden, nicht von grosser Bedeutung, da der Compiler dann sowieso den konstituierenden Ausdruck untersucht. Bleiben also groessere Funktionen. Ob noexcept hier einen nennenswerten Vorteil bringt... ist unklar, vielleicht möchten wir hier mal profilieren. Ich würde es fuer special member functions explizit markieren. Möglicherweise auch fuer Funktionen die hot sind.



  • @Columbo Okay. Also wenn ich das richtig verstanden habe, wäre der minimalistische Ansatz:

    Für Move-Operationen noexcept, für alles andere erstmal nix, es sei denn die Funktion wird später im Profiler auffällig.

    So ähnlich handhabe ich es derzeit auch, allerdings setze ich bei sehr leichtgewichtigen Abstraktionen doch noch sehr viel noexcept ein. Z.B. habe ich hier einen Iterator, der lediglich ein T& und einen Positions-int hält und wo ich sämtliche Member-Funktionen noexcept deklariert habe. Der soll sich letztendlich im Maschinencode in Nichts auflösen und den selben Code erzeugen wie eine for-Schleife mit int-Counter.

    Das fällt wohl letztendlich unter das, was du zu den Funktionen geschrieben hast, die geinlined werden, ist also wohl nicht wirklich notwendig. Ich werde das nochmal überdenken. Man sollte eigentlich davon ausgehen, dass der Compiler da selbst schlau genug ist, wenn lediglich Referenzen und ints herumgereicht werden.



  • @Finnegan IMO sollte man mit noexcept Spezifikationen sehr sparsam umgehen. Weil man, sobald man es selbst hinschreibt, keinerlei Support vom Compiler hat sicherzustellen dass es nicht gelogen ist.

    Z.B. habe ich hier einen Iterator, der lediglich ein T& und einen Positions-int hält und wo ich sämtliche Member-Funktionen noexcept deklariert habe.

    Wenn das alles inline implementiert ist, dann brauchst du da nichts noexcept zu machen. Weil der Compiler auch so sieht dass da keine Exceptions rausfliegen können.

    noexcept macht IMO hauptsächlich dann Sinn, wenn man noexcept Queries verwendet um explizit einen Algorithmus mit weniger Overhead auszuwählen. So wie es z.B. vector macht um zu entscheiden ob bei einer reallocation kopiert oder gemoved wird.

    Dummerweise gibt es aber noch kein noexcept(auto).

    Für Programme wo es nicht weiter tragisch ist wenn sie kontrolliert abkacken ist das nicht weiter schlimm. Wenn die Art wie ein Programm verwendet wird aber defensive Programmierung erfordert, ist noexcept dadurch aber leider im Moment kaum brauchbar.



  • Im Grunde muss jeder Destructor noexcept sein, weil es sonst beim Stack Unwinding zu Problemen kommen kann. std::unexcept hat man leider aus der Norm geworfen, aber das Stack Unwinding Problem in Zusammenhang mit Exception Handling ist noch immer nicht gelöst.



  • @john-0 sagte in Wann noexcept?:

    Im Grunde muss jeder Destructor noexcept sein, weil es sonst beim Stack Unwinding zu Problemen kommen kann. std::unexcept hat man leider aus der Norm geworfen, aber das Stack Unwinding Problem in Zusammenhang mit Exception Handling ist noch immer nicht gelöst.

    Glücklicherweise sind diese bereits implizit noexcept:

    Every function in C++ is either non-throwing or potentially throwing

    • potentially-throwing functions are:
      ...
      • functions declared without noexcept specifier except for
        • destructors unless the destructor of any potentially-constructed base or member is potentially-throwing (see below)

    Man muss sich also bei Destruktoren erst dann Gedanken um einen eventuellen noexcept-Specifier machen, wenn man auch tatsächlich aus dem Destruktor herauswerfen will - da hatte ich bisher zum Glück noch nie einen Anlass zu.

    Das ist gut, da ich ja letztendlich einfach nur möglichst wenig noexcept schreiben will, ohne dabei der Effizienz oder Korrektheit zu schaden.



  • @Finnegan sagte in Wann noexcept?:

    Glücklicherweise sind diese bereits implizit noexcept:

    Nein, das ist nicht der Fall, Du solltest Dir N3797 §15.4 Absatz 14 durchlesen. Der Destructor ist nur dann implizit noexcept, wenn er nur Funktionen aufruft die noexcept sind.


  • Mod

    @john-0 Das ist falsch/irreführend wiedergegeben (und Finnegan hat die relevante Passage doch sowieso mitzitiert). Der Destruktor leitet seine exception-specification von den Destruktoren der Subobjekte ab, deren unmittelbaren Aufruf er mit einfasst. Nicht etwa von allen Funktionen, die er potenziell aufruft.

    Da Destruktoren eben standardmäßig noexcept sind, wird letztlich auch jeder Destruktor noexcept bleiben, auch dann, wenn der Funktionskörper einen throw-Ausdruck beinhaltet, oder eine gewöhnliche, werfende Funktion aufruft.

    Bisjemand auf die bekloppte Idee kommt, tatsächlich noexcept(false) an einen Destruktor zu klatschen. Was ich noch nie gesehen habe.

    Wobei ich das Gefühl habe, dass sogar das oft zu noexcept(true) führen würde, weil die Funktionen, die gewöhnlich in Destruktoren Anwendung finden, ja doch noexcept sind, z.B. operator delete, Destruktoren von Standardbibliotheks-Klassen, etc. was natürlich rekursiv auf alle daraus aufgebauten Klassen zutrifft.

    @Finnegan Was ich bzgl. profilieren meinte, war eigentlich, dass wir (d.h. im Forum) mal eine mini-Studie machen sollten, in der wir ein paar solcher Fälle auf Optimierungen untersuchen. Einfach um ein Gefühl dafür zu bekommen, inwiefern das praktisch einen Unterschied macht.



  • @Columbo sagte in Wann noexcept?:

    @john-0 Das ist falsch/irreführend wiedergegeben (und Finnegan hat die relevante Passage doch sowieso mitzitiert).

    Da ich die aktuelle ISO Norm nicht zur Hand habe, habe ich auf den letzten Draft N3797 verwiesen und da findet sich folgender Absatz darin. Der entscheidende Absatz fett markiert.

    Als die relevante Stelle als Zitat
    An inheriting constructor (12.9) and an implicitly declared special member function (Clause 12) have an exception-specification. If f is an inheriting constructor or an implicitly declared default constructor, copy constructor, move constructor, destructor, copy assignment operator, or move assignment operator, its implicit exception-specification specifies the type-id T if and only if T is allowed by the exception-specification of a function directly invoked by f’s implicit definition; f allows all exceptions if any function it directly invokes allows all exceptions, and f has the exception-specification noexcept(true) if every function it directly invokes allows no exceptions.



  • @Columbo @john-0 Ich habe mir die referenzierte Passage gerade im aktuellen PDF durchgelesen und das Standardesisch auch so verstanden, dass für ein implizites noexcept auch jede aufgerufene Funktion noexcept sein muss. Das kann ja durchaus schnell passieren - z.B. gehen wahrscheinlich irgendwelche release-Funktionen einer C-Library-API meistens nicht wirklich als noexcept durch (sowas hätte ich z.B. öfter mal in den Destruktoren, die ich so schreibe).

    In dem Fall wäre mir tatsächlich ein explizites noexcept lieber, da ich bei dem dann eventuell auftretenden terminate-Call unmissverständlich merke, dass da etwas faul ist.

    Ich will eigentlich keine Exceptions in Destruktoren und denke, die sind auch immer irgendwie vermeidbar.

    @Columbo So eine "Mini-Studie" wäre sicher mal interessant. Ich poste hier zwar immer wieder gerne eigene Experimente, weiss aber nicht, ob ich Zeit für sowas finde. Mal schauen. Move-Konstruktor ohne noexcept und std::vector ist da sicher ne tief hängede Frucht, da lassen sich bestimmt gruselige Beispiele mit konstruieren, wenn Kopien nur teuer genug sind 😉

    Edit: Was mich bei dem Standard-Absatz allerdings stutzig macht, ist, dass nur von implicitly declared special member function/destructor die Rede ist. Diese sollten ja direkt eigentlich nur die Destruktoren der Basisklassen/Member aufrufen. Dafür ist das aber ganz schön umständlich formuliert 😉 ... uns geht es doch hier eher um das implizite noexcept von expliziten Destruktoren, oder?


  • Mod

    @Finnegan @john-0 Das ist doch gerade die Idee, dass man werfende Funktionen aufrufen kann, aber der Destruktor noexcept bleibt. Weil die beste allgemeine Strategie ist, dass eine Exception im Destruktor zu terminate führt.

    Das wording, dass von @john-0 zitiert worden ist, ist defekt, und wurde schon vor 8 Jahren zu korrigieren versucht: https://wg21.cmeerw.net/cwg/issue1351

    Wer mir immer noch nicht glaubt: https://coliru.stacked-crooked.com/a/054416897d790050



  • @Columbo sagte in Wann noexcept?:

    @Finnegan @john-0 Das ist doch gerade die Idee, dass man werfende Funktionen aufrufen kann, aber der Destruktor noexcept bleibt.

    Bitte lies Dir den Absatz ganz genau noch einmal durch!


  • Mod

    @john-0 Der aktuelle WD ist viel weiter als N3797 (welches von vor C++14 ist). Und die zitiere Passage ist, sowie @Finnegan aufgezeigt hat, doch gar nicht widersprüchlich, wegen implicitly declared, was meine Erklärung bestätigt. Wo liegt noch das Problem?



  • Sehe ich auch so wie @Finnegan: hier geht es um das implizite noexcept von expliziten Destruktoren (nicht um implizite Destruktoren, wie in dem Standard-Absatz beschrieben).



  • @Columbo sagte in Wann noexcept?:

    @john-0 Der aktuelle WD ist viel weiter als N3797 (welches von vor C++14 ist).

    Entschuldigung, die Webseite der C++ Working Group ist extrem schlecht. Wenn man da unter ISO 14882:2017 schaut, gibt es einen Link auf die ISO Publikation und direkt daneben einen Verweise auf N3797 und nicht etwa den passenden letzten Draft vor ISO/IEC 14882:2017.

    In letzten Draft vor ISO/IEC 14882:2017 ist es so wie von Dir beschrieben. cppreference.com dokumentiert leider nicht was aktueller Norm ist und was aktueller Working Draft ist.

    Leider kann sich die C++ Working Group nicht dazu durchringen, die offiziellen Normen frei zu publizieren, wie das etwa die Ada Conformty Assessment Authority macht. Dazu gibt es von Ada immer frei den Rationale dazu, in dem man nachlesen kann, weshalb etwas gemacht wurde. Das ist absolut vorbildlich und angesichts der Tatsache, dass C++ bedeutender ist um so unverständlicher.


Anmelden zum Antworten