noexcept und constexpr
« | 22 Aug 2021 | »noexcept
und constexpr
kamen mit C++11
in den Standard und sind zwei ganz tolle Features. Doch wegen der
Abwärtskompatibilität bleibt mir deren Nutzung im GATE Projekt eigentlich
verwehrt.
… oder eben nicht, wenn man ein bisschen trickst.
throw() != noexcept
Im MSVC könnte man eigentlich
fragen: Wozu braucht man noexcept
,
wenn die throw()
Exception-Specification
ohnehin schon Exception deaktiviert?
Es war tatsächlich eine Microsoft-spezifische Erweiterung (oder genau genommen
eine Reduktion), die bei einer Funktion mit throw()
Angabe das Generieren
von Exception-spezifischem-Binärcode einfach weggelassen hat.
In MSVC
war also das Werfen einer Exception innerhalb einer noexcept
Funktion “undefiniert” bzw. crash-reif, hat dafür aber schnell Code erzeugt,
weil Exceptions nicht behandelt wurden.
Doch gemäß Standard muss auf Exception zur Laufzeit immer geachtet werden: Doch gemäß Standard muss auf Exception zur Laufzeit immer geachtet werden:
throw()
ist eine Laufzeit Abweisung (Runtime) und sollte eigentlich immer eine Exception-Behandlung im Binärcode generieren und wenn in der Funktion eine nicht-erwartete Exception auftritt, mussstd::unexpected()
aufgerufen werden, wo man seinen eigenen Handler einsetzen könnte oder im Standardfallstd::terminate()
am Ende herauskommt.noexcept
bewirkt während der Übersetzung (Compiling) eine Prüfung, ob interner Code Exceptions wirft und gibt Warnungen aus, wenn einenoexcept
Funktion einen nicht-noexcept
Code nutzt. Ignoriert man die Warnung landet man auch beistd::terminate()
bei einer Exception aber zumindest ohne weitere Umwege und ohne viel Hintergrundcode.
const <= constexpr
constexpr
legt fest, dass der Wert eines Types schon beim Kompilieren
fix feststeht und das Ergebnis konstant bleibt und damit auch z.B. in
Templates als Parameter genutzt werden kann.
(Typische Beispiele mit Compile-Time-Faktoriellen-Berechnungen mit constexpr
sind offen gesagt ein Hohn und deshalb lasse ich sie hier weg. Denn seit dem
Jahr 2000 schaffen es fast alle Compiler solche Funktionen auch mit einem
normalen const
jeglichen Laufzeitcode wegzuoptimieren.)
constexpr
garantiert, dass das Ergebnis einer Operation konstant ist
und bleibt und im Idealfall finden wir bei dessen Nutzung im Binärcode dann
ebenfalls nur mehr eine CPU Operation, die einen statischen Wert nutzt.
Der Compiler kann also den Aufruf einer Funktion vollständig wegoptimieren
und während das bei const
alleine ein Glücksspiel ist, haben wir bei
constexpr
de facto eine Garantie für dieses Optimum.
Makros für Gestern
Tja, und wenn man jetzt mit MSVC 2005 kompiliert, führen noexcept
und
constexpr
zu Fehlern, da er sie einfach nicht kennt.
Doch wenn man auf alten Compilern ein einfaches
nutzt, werden zumindest ein paar modernere C++ Zeilen auch in antiken Varianten nutzbar. Sie generieren dort zwar nicht die gleichen guten Ergebnisse, gestatten aber zumindest eine formal korrekte Übersetzung.
So kann also problemlos noexcept
an Funktionen angefügt werden und andere
Funktionen dürfen constexpr
beim Rückgabetyp angeben.
Somit kann ich zumindest ein paar neue Ausdrücke im GATE Projekt unterbringen.
Die Nutzung von constexpr
Werten als Template-Parameter ist und bleibt
aber leider weiter Tabu.
Spezialfall MSVC 2013, Normalfall ab MSVC 2015
Und hier lernen wir wieder mal über Code-Archäologie, wie Microsoft nur
schleppend die neuen Standards umsetzte.
noexcept
wurde erst mit MSVC 2015 voll unterstützt, also brauchen ältere
MSVC Compiler also das obige Makro.
Doch leider galt noexcept
als reserviertes Schlüsselwort und dessen Nutzung
war in MSVC 2013 per #define
deshalb nicht erlaubt.
Dafür musste man das Makro _ALLOW_KEYWORD_MACROS
auf 1
definieren um
künstlich generierte Compiler-Fehler abzuwehren.
Fazit
constexpr
haben in GATE aktuell keine große Bedeutung, da viele relevante
Konstanten im C-Teil definiert sind und eigentlich nie über Funktionen
generiert werden.
Und für Integer-Konstanten in Templates gibt es ein besseres kompatibleres
Feature: nämlich enum
. Enum Werte sind Konstanten, Header-only-tauglich
und von C89 bis C++20 perfekt unterstützt. Ich ziehe daher ein
enum { abc = 42 };
einem constexpr int abc = 42
vor.
noexcept
hingegen ist sehr nützlich um besonders auf älteren Compilern
unnötigen Exception-Overhead zu reduzieren. Vor allem die Warnung bei
Nichteinhaltung einer noexcept
-Aufrufkette hilft enorm um Code sauber
zu halten.
Zwar sollten klassische extern C
Funktionen generell als noexcept
bzw. throw()
angenommen werden, doch gelegentlich wird dennoch im C++
Teil Exceptioncode generiert, der nie ausgeführt werden wird. Daher sollte
der Zusatz nie fehlen, wenn man keine Exceptions einsetzt.
Gerade auf 32-bit Systemen, wo Exceptionhandler über Zugriffe auf das F
Segment registriert werden, reduziert man so unnötig viel sinnlosen Code.
Tja … was man nicht alles aus Assembler-Dumps lernen kann …