noexcept und constexpr

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, muss std::unexpected() aufgerufen werden, wo man seinen eigenen Handler einsetzen könnte oder im Standardfall std::terminate() am Ende herauskommt.
  • noexcept bewirkt während der Übersetzung (Compiling) eine Prüfung, ob interner Code Exceptions wirft und gibt Warnungen aus, wenn eine noexcept Funktion einen nicht-noexcept Code nutzt. Ignoriert man die Warnung landet man auch bei std::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.

1template<int N> struct T { };
2
3int const i = 42;
4T<i> not_compilable;
5
6int constexpr j = 42;
7T<j> compilable;

(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

1#define noexcept throw()
2#define constexpr const

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 …