Réponse à la question
C++20 a introduit des types à virgule flottante en tant que paramètres de modèle non types (NTTP) en les classifiant comme des types structurels. Selon la norme ([temp.type]/4), deux arguments de modèle non types ne correspondent que s'ils sont équivalents. Pour les valeurs à virgule flottante, l'équivalence est déterminée par l'identité binaire plutôt que par l'égalité de valeur. Cela signifie que deux constantes à virgule flottante sont considérées comme le même argument de modèle uniquement si elles ont des représentations d'objet identiques (chaque bit correspond).
En conséquence, +0.0 et -0.0, qui diffèrent uniquement par leur bit de signe dans la représentation IEEE 754, instancient des modèles distincts. De même, différentes charges de NaN créent des types distincts. Cela contraste fortement avec le comportement à l'exécution où +0.0 == -0.0 évalue à true, car l'opérateur d'égalité met en œuvre l'équivalence mathématique tandis que le mécanisme de modèle exige une identité physique.
Situation de la vie réelle
Nous avons rencontré cela lors de la construction d'une bibliothèque d'analyse dimensionnelle à la compilation pour un moteur de simulation physique. Nous avons utilisé des NTTP en double pour représenter des constantes physiques (comme les constantes gravitationnelles) et souhaitions spécialiser des solveurs pour le cas théorique de la masse nulle (représentée comme 0.0). Cependant, certains calculs constexpr évaluant le centre de gravité produisaient -0.0 par des opérations arithmétiques spécifiques (par exemple, -1.0 * 0.0).
Lorsque les utilisateurs ont passé le résultat de ces calculs comme argument de modèle, le compilateur a sélectionné l'implémentation générique au lieu de notre spécialisation ZeroMass, ce qui a entraîné une régression de performance de 40 % car la version générique effectuait des inversions complètes de matrices au lieu de renvoyer des matrices identitaires.
Nous avons considéré trois solutions. Premièrement, nous pourrions spécialiser explicitement pour +0.0 et -0.0. Cette approche garantissait un comportement correct mais doublait notre charge de maintenance et ne parvenait toujours pas à gérer diverses représentations NaN ou des valeurs qui étaient effectivement nulles mais avaient des motifs de bits différents en raison d'erreurs d'arrondi.
Deuxièmement, nous avons envisagé de normaliser toutes les entrées à l'aide d'une fonction d'assistance constexpr qui forçait le bit de signe à zéro (par exemple, value == 0.0 ? 0.0 : value). Cette solution était robuste pour les zéros mais nécessitait des macros d'enveloppe autour de chaque instanciation de modèle, polluant l'API et confondant les utilisateurs qui s'attendaient à un passage direct des paramètres.
Troisièmement, nous avons mis en œuvre une couche de normalisation de type utilisant if constexpr et std::bit_cast pour canoniser les valeurs au point d'entrée de nos méta-fonctions, traitant effectivement tous les zéros comme positifs et réduisant les NaNs silencieuses à une charge canonique. Nous avons choisi cette solution car elle offrait une transparence aux utilisateurs de la bibliothèque tout en garantissant une cohérence interne.
Après mise en œuvre, nous avons documenté que la bibliothèque traitait tous les NTTP à virgule flottante selon leur représentation binaire. Cela a résolu les problèmes de performance, bien qu'il ait fallu que les développeurs soient conscients que -0.0 et +0.0 étaient des états de configuration distincts dans le système de types.
Ce que les candidats oublient souvent
Pourquoi std::is_same_v<decltype(func<+0.0>()), decltype(func<-0.0>())> évalue-t-il à faux alors que +0.0 == -0.0 est vrai ?
L'instanciation des modèles repose sur la règle d'une seule définition et une correspondance exacte des arguments de modèle. Lorsque le compilateur rencontre func<+0.0>(), il hache ou compare le motif binaire du littéral à virgule flottante. Étant donné que IEEE 754 spécifie que -0.0 a son bit de signe défini tandis que +0.0 ne l'a pas, le compilateur voit deux valeurs constantes différentes et génère deux instanciations de fonction distinctes. L'opérateur d'égalité à l'exécution met en œuvre la spécification IEEE 754 selon laquelle les zéros signés se comparent comme égaux, mais la machinerie du modèle opère au niveau de la représentation d'objet avant que les sémantiques d'exécution ne s'appliquent. Les candidats supposent souvent qu'étant donné que les valeurs sont mathématiquement équivalentes, elles devraient produire le même type, confondant les sémantiques de valeur à l'exécution avec l'identité de type à la compilation.
Pourquoi template<float F> struct S{}; S<1.0> échoue-t-il à compiler bien que 1.0 soit implicitement convertible en float dans des expressions normales ?
Pour les paramètres de modèle non types de type à virgule flottante, la norme C++20 exige explicitement que l'argument de modèle ait exactement le même type que le paramètre ; les promotions et conversions de types à virgule flottante standard ne sont pas permises ([temp.arg.nontype]/5). Le littéral 1.0 a le type double, pas float, donc il ne peut pas se lier directement à float F. Vous devez utiliser le suffixe float : S<1.0f>. Cette restriction existe car le mangling des modèles et l'identité des types nécessitent une représentation sans ambiguïté sans perte de précision lors de la conversion. Les débutants manquent souvent ceci car les appels de fonction permettent la conversion, mais les modèles effectuent une correspondance exacte des types avant que les règles de conversion ne soient considérées.
Comment les différentes charges NaN silencieuses (qNaN) affectent-elles l'instanciation des modèles lorsqu'elles représentent toutes "pas un nombre" ?
IEEE 754 permet aux valeurs NaN de transporter des bits de charge (informations de diagnostic). Étant donné que l'équivalence des modèles en C++20 utilise la comparaison binaire, deux NaNs avec des charges différentes (par exemple, std::numeric_limits<double>::quiet_NaN() contre le résultat de 0.0/0.0 sur différentes plateformes) sont des arguments de modèle distincts. Cela peut conduire à un gonflement du code si les chemins de code instancient des modèles pour plusieurs motifs de bits NaN, ou à des violations subtiles de ODR si différentes unités de traduction observent des représentations NaN différentes pour ce que le programmeur supposait être une seule spécialisation. Les candidats supposent fréquemment que NaN est une valeur unique comme nullptr, mais elle représente en réalité une gamme de motifs de bits, chacun distinct dans le système de modèles.