Antwort auf die Frage
C++20 führte Gleitpunkttypen als Nicht-Typ-Template-Parameter (NTTPs) ein, indem es sie als strukturelle Typen klassifizierte. Laut dem Standard ([temp.type]/4) stimmen zwei Nicht-Typ-Template-Argumente nur überein, wenn sie äquivalent sind. Bei Gleitpunktwerten wird die Äquivalenz durch bitweise Identität und nicht durch Wertgleichheit bestimmt. Das bedeutet, dass zwei Gleitpunktkonstanten nur dann als dasselbe Template-Argument betrachtet werden, wenn sie identische Objektrepräsentationen aufweisen (jedes Bit stimmt überein).
Folglich instanziieren +0.0 und -0.0, die sich nur im Vorzeichenbit unter der IEEE 754-Darstellung unterscheiden, unterschiedliche Templates. Ähnlich erzeugen verschiedene NaN-Nutzlasten unterschiedliche Typen. Dies steht im scharfen Gegensatz zum Laufzeitverhalten, bei dem +0.0 == -0.0 zu true ausgewertet wird, da der Gleichheitsoperator mathematische Äquivalenz implementiert, während der Template-Mechanismus physische Identität erfordert.
Situation aus dem Leben
Wir begegneten diesem Problem, während wir eine Compile-Time-Dimensionalanalysebibliothek für eine Physik-Simulations-Engine entwickelten. Wir verwendeten double NTTPs, um physikalische Konstanten (wie Gravitationskonstanten) darzustellen und wollten Solver für den theoretischen Fall einer Nullmasse (dargestellt als 0.0) spezialisieren. Einige constexpr-Berechnungen, die den Schwerpunkt berechneten, erzeugten jedoch -0.0 durch spezifische arithmetische Operationen (z. B. -1.0 * 0.0).
Als die Benutzer das Ergebnis dieser Berechnungen als Template-Argument übergaben, wählte der Compiler die generische Implementierung anstelle unserer ZeroMass-Spezialisierung, was zu einem Leistungsrückgang von 40 % führte, da die generische Version vollständige Matrixinversionen durchführte, anstatt Identitätsmatrizen zurückzugeben.
Wir überlegten uns drei Lösungen. Erstens konnten wir explizit für sowohl +0.0 als auch -0.0 spezialisieren. Dieser Ansatz gewährte korrektes Verhalten, verdoppelte aber unsere Wartungslast und konnte immer noch verschiedene NaN-Darstellungen oder Werte, die effektiv null waren, nicht handhaben, aber aufgrund von Rundungsfehlern unterschiedliche Bitmuster aufwiesen.
Zweitens erwogen wir, alle Eingaben mithilfe einer constexpr-Hilfsfunktion zu normalisieren, die das Vorzeichenbit auf null zwang (z. B. value == 0.0 ? 0.0 : value). Diese Lösung war robust für Nullen, erforderte jedoch Wrapper-Makros um jede Template-Instanziierung, was die API verunreinigte und Benutzer verwirrte, die eine direkte Parameterübergabe erwarteten.
Drittens implementierten wir eine Typnormalisierungsschicht unter Verwendung von if constexpr und std::bit_cast, um Werte an den Eingabepunkten unserer Metafunktionen zu kanonisieren, wodurch alle Nullen als positiv behandelt und stille NaNs zu einer kanonischen Nutzlast zusammengeführt wurden. Wir wählten diese Lösung, weil sie den Benutzern der Bibliothek Transparenz bot und gleichzeitig interne Konsistenz gewährte.
Nach der Implementierung dokumentierten wir, dass die Bibliothek alle Gleitpunkt-NTTPs anhand ihrer Bitdarstellung behandelte. Dies löste die Leistungsprobleme, erforderte jedoch, dass die Entwickler sich bewusst waren, dass -0.0 und +0.0 unterschiedliche Konfigurationszustände im Typsystem darstellten.
Was Kandidaten oft übersehen
Warum ergibt std::is_same_v<decltype(func<+0.0>()), decltype(func<-0.0>())> false, wenn +0.0 == -0.0 true ist?
Die Template-Instanziierung beruht auf der One Definition Rule und genauem Matching der Template-Argumente. Wenn der Compiler func<+0.0>() trifft, hasht oder vergleicht er das Bitmuster des Gleitpunktliteral. Da IEEE 754 angibt, dass -0.0 sein Vorzeichenbit gesetzt hat, während +0.0 dies nicht hat, sieht der Compiler zwei unterschiedliche konstante Werte und erzeugt zwei verschiedene Funktionsinstanziierungen. Der Gleichheitsoperator zur Laufzeit implementiert die IEEE 754-Spezifikation, dass signierte Nullen gleich sind, aber die Template-Mechanik arbeitet auf dem Niveau der Objektrepräsentation, bevor die Laufzeitsemantik angewendet wird. Kandidaten gehen oft fälschlicherweise davon aus, dass, weil die Werte mathematisch äquivalent sind, sie denselben Typ ergeben sollten, und verwechseln somit Laufzeitwertsemantik mit Compile-Zeit-Typidentität.
Warum schlägt template<float F> struct S{}; S<1.0> fehl, obwohl 1.0 in normalen Ausdrücken implizit in float umgewandelt werden kann?
Bei Nicht-Typ-Template-Parametern des Gleitpunkttyps verlangt der C++20-Standard ausdrücklich, dass das Template-Argument den exakt selben Typ wie der Parameter hat; standardmäßige Gleitpunkt-Promotionen und -Konversionen sind nicht erlaubt ([temp.arg.nontype]/5). Der Literal 1.0 hat den Typ double, nicht float, daher kann er nicht direkt an float F gebunden werden. Sie müssen den float-Suffix verwenden: S<1.0f>. Diese Einschränkung besteht, weil das Template-Mangling und die Typidentität eine eindeutige Darstellung ohne Verlust der Umwandlungsgenauigkeit erfordern. Anfänger übersehen dies oft, weil Funktionsaufrufe die Umwandlung erlauben, aber Templates eine exakte Typübereinstimmung vor der Berücksichtigung von Umwandlungsregeln durchführen.
Wie beeinflussen verschiedene stille NaN (qNaN)-Nutzlasten die Template-Instanziierung, wenn sie alle "nicht eine Zahl" repräsentieren?
IEEE 754 erlaubt NaN-Werte, Payload-Bits (Diagnoseinformationen) zu tragen. Da die Äquivalenz von Templates in C++20 bitweise Vergleiche verwendet, sind zwei NaNs mit unterschiedlichen Nutzlasten (z. B. std::numeric_limits<double>::quiet_NaN() im Vergleich zum Ergebnis von 0.0/0.0 auf unterschiedlichen Hardware) unterschiedliche Template-Argumente. Dies kann zu Code-Aufblähung führen, wenn Code-Pfade Templates für mehrere NaN-Bitmuster instanziieren oder zu subtilen ODR-Verletzungen führen, wenn unterschiedliche Übersetzungseinheiten unterschiedliche NaN-Darstellungen beobachten, für die der Programmierer annahm, dass es sich um eine einzelne Spezialisierung handelt. Kandidaten nehmen häufig an, dass NaN ein einzelner Wert wie nullptr ist, aber es stellt tatsächlich eine Reihe von Bitmustern dar, die im Template-System jeweils unterschiedlich sind.