History: Const generics were stabilized in Rust 1.51 to allow types to be parameterized by constant values of primitive integer types, enabling generic fixed-size arrays like [T; N]. During the design phase, the language team explicitly restricted const generic parameters to types that exhibit structural equality and deterministic compile-time evaluation. This restriction excluded f32, f64, and &str literals due to their violation of total ordering or their dependence on runtime memory addresses.
Problem: The core issue with floating-point types is the presence of NaN (Not-a-Number), which violates reflexive equality (NaN != NaN), preventing the compiler from reliably determining type identity during monomorphization. For string literals (&str), the problem lies in their fat pointer representation (address + length) and their dependence on specific memory addresses in the data segment, which are not deterministic across compilation units or crates. The type system requires that MyStruct<1> and MyStruct<1> always refer to the identical type, necessitating that the const parameter's equality be decidable through bitwise or structural comparison at compile time.
Solution: The Rust compiler enforces these constraints through internal traits like StructuralPartialEq (unstable) during HIR (High-Level Intermediate Representation) lowering and type checking. When encountering a const generic parameter, the compiler verifies that the type is an integer, bool, or char, or a user-defined type explicitly marked as supporting structural equality. It rejects floating-point types because their equality is not reflexive, and rejects references like &str because they introduce lifetimes and indirection that cannot be reconciled in the 'static context required for const generics. During monomorphization, the compiler evaluates const expressions and uses structural equality to merge identical instantiations, ensuring type safety.
// Valid: usize has structural equality struct Matrix<const N: usize> { data: [[f64; N]; N], } // Invalid: f64 lacks total ordering (NaN issues) // struct Physics<const G: f64>; // Error: floating-point types cannot be used in const generics // Invalid: &str has indirection and lifetime complexity // struct Label<const S: &str>; // Error: `&str` is forbidden as the type of a const generic parameter
You are architecting a high-frequency trading engine where financial instruments must carry compile-time constant parameters for contract specifications, such as tick sizes (e.g., 0.25 USD) or multiplier coefficients. The initial design attempted to use f64 const generics to encode these precise decimal values directly into the type system, hoping to eliminate runtime storage of these constants and enable compile-time optimization of pricing calculations.
One approach considered was bypassing the restriction by transmuting f64 bits into u64 and using that as the const parameter, then transmuting back within the implementation. However, this proved hazardous because bitwise identical floats can represent different semantic values due to signed zero (+0.0 vs -0.0) and NaN payloads, potentially causing the compiler to treat distinct financial instruments as the same type or merging calculations that should remain separate, leading to incorrect pricing logic.
Another solution involved using associated constants within a trait (trait Instrument { const TICK_SIZE: f64; }). While this allows floating-point values, it sacrifices the ability to use the tick size as a type-level discriminator; you cannot have Vec<Instrument<TICK_SIZE>> containing different instruments with different tick sizes without resorting to dyn Trait object overhead, which introduces vtable indirection unacceptable in the hot path.
The chosen solution was to encode the floating-point values as fixed-point integers (e.g., representing 0.25 USD as the usize 25 with an implicit scaling factor of 100). This approach satisfies the const generic constraints while maintaining zero-cost abstraction and compile-time evaluation. The result was a type-safe contract system where Bond<25> and Bond<50> are distinct types with no runtime overhead, though it required careful documentation of the scaling convention to prevent arithmetic errors.
Why does Rust permit char and bool as const generic parameters but excludes &str, given that both are technically primitive types?
Char and bool are value types with fixed sizes and trivial structural equality; a char is a 32-bit Unicode scalar value and bool is strictly 0 or 1, allowing bitwise comparison. &str is a fat pointer (or reference to a DST) containing a data pointer and a length, introducing indirection and lifetime parameters. The compiler cannot guarantee that two string literals reside at the same memory address across different crates or that their lifetimes satisfy the 'static requirements in a way that permits type identity checking. Consequently, &str lacks the structural properties required for const generic parameters, whereas char and bool are self-contained values.
How would implementing const generics for floating-point types potentially break type safety regarding NaN (Not-a-Number) values?
If f32 were permitted, expressions like MyStructf32::NAN and MyStruct<{ 0.0 / 0.0 }> would both produce NaN values, but the compiler could not guarantee they represent the same type because NaN != NaN. This would allow the creation of two distinct monomorphizations of what should logically be the same type, or conversely, force the compiler to incorrectly merge types that contain different NaN payloads. This violation of type identity could lead to unsoundness where singleton patterns fail or where type-based optimizations produce incorrect code, as the compiler assumes type parameters uniquely identify a single type.
What is the fundamental distinction between const generics and associated constants, and why does the former require structural equality while the latter does not?
Const generic parameters are part of the type identity; Container<10> and Container<20> are distinct types with separate monomorphizations. This requires that the values be comparable at compile time to ensure global uniqueness and to merge identical instantiations. Associated constants are values associated with a type implementation but do not alter the type itself; TypeA and TypeB remain distinct types regardless of their associated constant values. Therefore, associated constants can be floating-point or complex types because they merely provide values within the implementation without affecting type checking or monomorphization, bypassing the need for structural equality at the type system level.