Floating-Point Non-Type Template Parameters in C++

This article provides an overview of the constraints and rules governing the use of floating-point non-type template parameters (NTTP) in C++. While early versions of the standard prohibited floating-point types in template parameter lists, C++20 officially added support for them under strict structural conditions. Below is a detailed breakdown of the requirements, equivalence guarantees, and edge-case behaviors that apply when working with floating-point NTTPs.

C++ Standard Version Requirement

Floating-point non-type template parameters are only permitted starting in C++20. In earlier standards (C++98 through C++17), non-type template parameters were restricted to integral types, enumeration types, pointers, references, and std::nullptr_t. Code using floating-point types such as float, double, or long double as template arguments will fail to compile on standards prior to C++20.

The Structural Type Requirement

Under C++20, an NTTP must be a structural type. A type is structural if it is:

  • A scalar type (which includes standard floating-point types).
  • An lvalue reference type.
  • A literal class type meeting specific criteria regarding public, non-mutable subobjects.

Because standard floating-point types (float, double, long double) are scalar types, they inherently satisfy the structural type requirement.

Compile-Time Constant Expressions

Any floating-point value passed to a template must be a core constant expression known at compile time. It must be evaluatable during translation using constexpr semantics. Runtime variables, non-constant functions, or operations that produce undefined behavior during constant evaluation cannot be used as arguments.

Template Argument Equivalence

A critical challenge with floating-point values in templates is determining whether two distinct template instantiations refer to the exact same type or function. C++20 establishes precise rules for template argument equivalence:

  1. Equality of Values: Two values of the same floating-point type generate the same template specialization if they evaluate to the same value.
  2. Signed Zeros (+0.0 and -0.0): In standard IEEE 754 arithmetic, +0.0 == -0.0 is true, despite having different bit representations. In C++, template arguments +0.0 and -0.0 are considered template-argument-equivalent and yield the same specialization.
  3. Not-a-Number (NaN): While IEEE 754 defines NaN == NaN as false, C++ template rules treat two quiet NaN values of the same type as template-argument-equivalent, resolving them to the same template specialization.

Implementation and Optimization Flags

Compilers rely on strict IEEE 754 compliance to maintain predictable behavior across translation units. Using aggressive optimization flags—such as -ffast-math or -Ofast in GCC and Clang—can alter algebraic rules, zero signedness, and NaN handling. This can cause non-standard behavior or inconsistencies in how floating-point template arguments are evaluated at compile time.