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 vonstd::vector
z.B. arbeiten aufgrund ihrer Strong Exception Guarantee intern mitstd::move_if_noexcept
o.ä., wenn Elemente verschoben werden müssen. Wenn die Move-Operation nichtnoexcept
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 gegennoexcept
, aber es ist schon ein zusäzlicher Komplexitäts-Layer mit nicht immer klarem Nutzen).
-
Es gibt zwei Wege, auf die noexcept-ness das Program beeinflussen kann.
- 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. - 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.
- Der Compiler sieht, dass ein gewisser Ausdruck keine Exception werfen kann (weil während der Auswertung sonst
-
@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 einT&
und einen Positions-int
hält und wo ich sämtliche Member-Funktionennoexcept
deklariert habe. Der soll sich letztendlich im Maschinencode in Nichts auflösen und den selben Code erzeugen wie einefor
-Schleife mitint
-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
int
s 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 mannoexcept
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)
- functions declared without noexcept specifier except for
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.
- potentially-throwing functions are:
-
@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.
-
@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 Destruktornoexcept
bleiben, auch dann, wenn der Funktionskörper einenthrow
-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 dochnoexcept
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 Funktionnoexcept
sein muss. Das kann ja durchaus schnell passieren - z.B. gehen wahrscheinlich irgendwelcherelease
-Funktionen einer C-Library-API meistens nicht wirklich alsnoexcept
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 auftretendenterminate
-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
undstd::vector
ist da sicher ne tief hängede Frucht, da lassen sich bestimmt gruselige Beispiele mit konstruieren, wenn Kopien nur teuer genug sindEdit: 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?
-
@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 zuterminate
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!
-
@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.