Storia: I Generici Const sono stati stabilizzati in Rust 1.51 per consentire ai tipi di essere parametrizzati da valori costanti di tipi interi primitivi, consentendo array generici a dimensione fissa come [T; N]. Durante la fase di progettazione, il team di linguaggio ha esplicitamente vietato i parametri generici const a tipi che presentano uguaglianza strutturale e valutazione deterministica a tempo di compilazione. Questa restrizione ha escluso f32, f64 e litere &str a causa della loro violazione dell'ordinamento totale o della loro dipendenza dagli indirizzi di memoria a runtime.
Problema: Il problema centrale con i tipi floating-point è la presenza di NaN (Not-a-Number), che violano l'uguaglianza riflessiva (NaN != NaN), impedendo al compilatore di determinare affidabilmente l'identità del tipo durante la monomorfizzazione. Per le litere di stringa (&str), il problema risiede nella loro rappresentazione del puntatore grasso (indirizzo + lunghezza) e nella loro dipendenza da indirizzi di memoria specifici nel segmento dati, che non sono deterministici tra unità di compilazione o crate. Il sistema di tipi richiede che MyStruct<1> e MyStruct<1> facciano sempre riferimento allo stesso tipo, richiedendo che l'uguaglianza del parametro const sia decidibile tramite confronto bitwise o strutturale a tempo di compilazione.
Soluzione: Il compilatore Rust applica queste restrizioni attraverso tratti interni come StructuralPartialEq (non stabile) durante la riduzione a HIR (Rappresentazione Intermedia di Alto Livello) e il controllo dei tipi. Quando il compilatore incontra un parametro generico const, verifica che il tipo sia un intero, bool o char, o un tipo definito dall'utente specificamente contrassegnato come supportante uguaglianza strutturale. Rifiuta i tipi floating-point perché la loro uguaglianza non è riflessiva e rifiuta riferimenti come &str perché introducono complessità di vita e indirection che non possono essere riconciliate nel contesto 'static richiesto per i generici const. Durante la monomorfizzazione, il compilatore valuta le espressioni const e utilizza l'uguaglianza strutturale per unire le istanze identiche, garantendo la sicurezza dei tipi.
// Valido: usize ha uguaglianza strutturale struct Matrix<const N: usize> { data: [[f64; N]; N], } // Non valido: f64 non ha ordinamento totale (problemi NaN) // struct Physics<const G: f64>; // Errore: i tipi floating-point non possono essere utilizzati nei generici const // Non valido: &str ha complessità di indirection e durata // struct Label<const S: &str>; // Errore: `&str` è vietato come tipo di un parametro generico const
Stai progettando un motore di trading ad alta frequenza in cui gli strumenti finanziari devono comportare parametri costanti a tempo di compilazione per le specifiche contrattuali, come le dimensioni dei tick (ad es., 0.25 USD) o i coefficienti moltiplicatori. La progettazione iniziale ha tentato di utilizzare generici const f64 per codificare direttamente questi valori decimali precisi all'interno del sistema di tipi, sperando di eliminare lo storage runtime di queste costanti e abilitare l'ottimizzazione a tempo di compilazione dei calcoli di prezzo.
Un approccio considerato è stato quello di aggirare la restrizione tramutando i bit di f64 in u64 e utilizzando quello come parametro const, per poi riconvertirlo durante l'implementazione. Tuttavia, questo si è rivelato pericoloso poiché i float bitwise identici possono rappresentare valori semantici diversi a causa dello zero firmato (+0.0 vs -0.0) e dei payload NaN, potenzialmente causando al compilatore di trattare strumenti finanziari distinti come lo stesso tipo o di unire calcoli che dovrebbero rimanere separati, portando a una logica di prezzo errata.
Un'altra soluzione prevedeva di utilizzare costanti associate all'interno di un tratto (trait Instrument { const TICK_SIZE: f64; }). Anche se ciò consente valori floating-point, sacrifica la possibilità di utilizzare la dimensione del tick come discriminatore a livello tipo; non puoi avere Vec<Instrument<TICK_SIZE>> contenente strumenti diversi con dimensioni di tick diverse senza ricorrere a un overhead dell'oggetto dyn Trait, che introduce un'indirection tramite vtable inaccettabile nel percorso caldo.
La soluzione scelta è stata quella di codificare i valori floating-point come interi a punto fisso (ad es., rappresentando 0.25 USD come usize 25 con un fattore di scala implicito di 100). Questo approccio soddisfa i vincoli generici const pur mantenendo un'astrazione a costo zero e valutazione a tempo di compilazione. Il risultato è stato un sistema contrattuale sicuro per i tipi in cui Bond<25> e Bond<50> sono tipi distinti senza overhead runtime, sebbene richiedesse una documentazione attenta della convenzione di scala per prevenire errori aritmetici.
Perché Rust consente char e bool come parametri generici const ma esclude &str, dato che entrambi sono tecnicamente tipi primitivi?
Char e bool sono tipi di valore con dimensioni fisse e uguaglianza strutturale banale; un char è un valore scalare Unicode a 32 bit e bool è strictly 0 o 1, consentendo il confronto bitwise. &str è un puntatore grasso (o riferimento a un DST) contenente un puntatore ai dati e una lunghezza, introducendo indirection e parametri di durata. Il compilatore non può garantire che due litere di stringa risiedano allo stesso indirizzo di memoria tra diversi crate o che le loro durate soddisfino i requisiti 'static in un modo che consenta il controllo dell'identità del tipo. Di conseguenza, &str manca delle proprietà strutturali richieste per i parametri generici const, mentre char e bool sono valori autonomi.
In che modo l'implementazione di generici const per i tipi floating-point potrebbe potenzialmente violare la sicurezza dei tipi riguardo ai valori NaN (Not-a-Number)?
Se f32 fosse consentito, espressioni come MyStructf32::NAN e MyStruct<{ 0.0 / 0.0 }> produrrebbero entrambi valori NaN, ma il compilatore non potrebbe garantire che rappresentino lo stesso tipo perché NaN != NaN. Questo permetterebbe la creazione di due monomorfizzazioni distinte di ciò che dovrebbe logicamente essere lo stesso tipo, o viceversa, costringerebbe il compilatore a unire erroneamente tipi che contengono payload NaN diversi. Questa violazione dell'identità del tipo potrebbe portare a insicurezze in cui i pattern singleton falliscono o dove le ottimizzazioni basate sui tipi producono codice errato, poiché il compilatore assume che i parametri di tipo identificano unicamente un singolo tipo.
Qual è la distinzione fondamentale tra generici const e costanti associate, e perché i primi richiedono uguaglianza strutturale mentre i secondi no?
I parametri generici const fanno parte dell'identità del tipo; Container<10> e Container<20> sono tipi distinti con monomorfizzazioni separate. Ciò richiede che i valori siano confrontabili a tempo di compilazione per garantire l'unicità globale e unire le istanze identiche. Le costanti associate sono valori associati a un'implementazione del tipo ma non alterano il tipo stesso; TypeA e TypeB rimangono tipi distinti indipendentemente dai loro valori di costante associata. Pertanto, le costanti associate possono essere di tipo floating-point o complesso perché forniscono semplicemente valori all'interno dell'implementazione senza influenzare il controllo dei tipi o la monomorfizzazione, eludendo la necessità di uguaglianza strutturale a livello del sistema di tipi.