Ответ на вопрос
C++20 ввел типы с плавающей запятой в качестве аргументов шаблона без типа (NTTP), классифицируя их как структурные типы. Согласно стандарту ([temp.type]/4), два аргумента шаблона без типа совпадают только в том случае, если они эквивалентны. Для значений с плавающей запятой эквивалентность определяется по битовому совпадению, а не по равенству значений. Это означает, что два константных значения с плавающей запятой считаются одним и тем же аргументом шаблона только в том случае, если они имеют идентичные представления объектов (каждый бит совпадает).
Соответственно, +0.0 и -0.0, которые различаются только знаком, в представлении IEEE 754 создают разные шаблоны. Аналогично, разные полезные нагрузки NaN создают разные типы. Это резко контрастирует с поведением во время выполнения, где +0.0 == -0.0 оценивается как true, потому что оператор равенства реализует математическую эквивалентность, в то время как механизм шаблонов требует физической идентичности.
Ситуация из жизни
Мы столкнулись с этим, создавая библиотеку статического анализа размерностей для физического симулятора. Мы использовали double NTTP для представления физических констант (таких как гравитационные постоянные) и хотели специализированные решатели для теоретического случая нулевой массы (представленного как 0.0). Однако некоторые constexpr расчеты, оценивающие центры масс, давали -0.0 из-за специфических арифметических операций (например, -1.0 * 0.0).
Когда пользователи передавали результат этих расчетов в качестве аргумента шаблона, компилятор выбирал обобщенную реализацию вместо нашей специализации ZeroMass, что приводило к снижению производительности на 40%, потому что обобщенная версия выполняла полные матричные инверсии вместо возврата единичных матриц.
Мы рассмотрели три решения. Во-первых, мы могли бы явно специализировать для +0.0 и -0.0. Этот подход гарантировал бы правильное поведение, но утяжелял бы нашу поддержку и по-прежнему не справлялся с различными представлениями NaN или значениями, которые по сути были нулевыми, но имели разные битовые шаблоны из-за ошибок округления.
Во-вторых, мы рассмотрели нормализацию всех входных данных с помощью вспомогательной функции constexpr, которая принуждала бит знака к нулю (например, value == 0.0 ? 0.0 : value). Это решение было надежным для нулей, но требовало оберток макросов вокруг каждой инстанциации шаблона, что загрязняло API и путало пользователей, которые ожидали непосредственной передачи параметров.
В-третьих, мы реализовали слой нормализации типов, используя if constexpr и std::bit_cast для канификации значений на входной точке наших метафункций, эффективно рассматривая все нули как положительные и сводя тихие NaN к канонической полезной нагрузке. Мы выбрали это решение, потому что оно обеспечивало прозрачность для пользователей библиотеки, сохраняя внутреннюю согласованность.
После реализации мы задокументировали, что библиотека рассматривает все NTTP с плавающей запятой по их битовому представлению. Это решило проблемы с производительностью, хотя и требовало от разработчиков осознания того, что -0.0 и +0.0 были различными состояниями конфигурации в системе типов.
Что часто пропускают кандидаты
Почему std::is_same_v<decltype(func<+0.0>()), decltype(func<-0.0>())> оценивается как false, когда +0.0 == -0.0 истинно?
Инстанциация шаблона зависит от Правила Единственного Определения и точного совпадения аргументов шаблона. Когда компилятор сталкивается с func<+0.0>(), он хэширует или сравнивает битовый шаблон литерала с плавающей запятой. Поскольку IEEE 754 указывает, что -0.0 имеет установленный бит знака, в то время как +0.0 нет, компилятор видит два разных константных значения и генерирует две различные инстанциации функции. Оператор равенства во время выполнения реализует спецификацию IEEE 754, согласно которой знаковые нули сравниваются как равные, но механизм шаблонов работает на уровне представления объектов до того, как применяются семантика времени выполнения. Кандидаты часто предполагают, что поскольку значения математически эквивалентны, они должны производить один и тот же тип, путая семантику значений времени выполнения с идентичностью типов времени компиляции.
Почему template<float F> struct S{}; S<1.0> не компилируется, несмотря на то что 1.0 может быть неявно преобразован в float в обычных выражениях?
Для аргументов шаблона без типа с плавающей запятой стандарт C++20 четко требует, чтобы аргумент шаблона имел точно такой же тип, как параметр; стандартные преобразования и продвижения с плавающей запятой не допускаются ([temp.arg.nontype]/5). Литерал 1.0 имеет тип double, а не float, поэтому он не может быть связан непосредственно с float F. Вам нужно использовать суффикс float: S<1.0f>. Это ограничение существует, потому что устранение двойственности в названии типов и идентичность типов требуют однозначного представления без потери точности преобразования. Начинающие программисты часто пропускают это потому, что вызовы функций допускают преобразование, но шаблоны выполняют точное соответствие типов перед тем, как учитываются правила преобразования.
Как разные полезные нагрузки тихих NaN (qNaN) влияют на инстанциацию шаблонов, когда все они представляют "не число"?
IEEE 754 позволяет NaN значениям содержать биты полезной нагрузки (диагностическую информацию). Поскольку эквивалентность шаблонов C++20 использует битовое сравнение, два NaN с различными полезными нагрузками (например, std::numeric_limits<double>::quiet_NaN() по сравнению с результатом 0.0/0.0 на различном оборудовании) являются различными аргументами шаблона. Это может привести к увеличению объема кода, если пути кода инстанцируют шаблоны для нескольких битовых шаблонов NaN, или к тонким нарушениям ODR, если различные единицы перевода наблюдают различные представления NaN для того, что программист считал одной специализацией. Кандидаты часто предполагают, что NaN — это одно единственное значение, как nullptr, но на самом деле оно представляет собой множество битовых шаблонов, каждый из которых различен в системе шаблонов.