Historia: Ogólne parametry const zostały ustabilizowane w Rust 1.51, aby umożliwić parametryzację typów stałymi wartościami typów całkowitych, co pozwala na tworzenie ogólnych tablic o stałej wielkości, takich jak [T; N]. Podczas fazy projektowania zespół językowy wyraźnie ograniczył parametry ogólne const do typów, które wykazują równość strukturalną i deterministyczną ewaluację w czasie kompilacji. To ograniczenie wykluczyło f32, f64 oraz literały &str z powodu ich naruszania pełnego porządku lub ich zależności od adresów pamięci w czasie wykonywania.
Problem: Kluczowym problemem z typami zmiennoprzecinkowymi jest obecność NaN (Nie-liczba), która narusza równość refleksywną (NaN != NaN), co uniemożliwia kompilatorowi niezawodne określenie tożsamości typów podczas monomorfizacji. W przypadku literałów łańcuchowych (&str) problem leży w ich reprezentacji wskaźnika grubego (adres + długość) i ich zależności od konkretnych adresów pamięci w segmencie danych, które nie są deterministyczne w różnych jednostkach kompilacji lub paczkach. System typów wymaga, aby MyStruct<1> i MyStruct<1> zawsze odnosiły się do identycznego typu, co wymaga, aby równość parametrów const była rozstrzygalna przez porównanie bitowe lub strukturalne w czasie kompilacji.
Rozwiązanie: Kompilator Rust egzekwuje te ograniczenia za pomocą wewnętrznych cech, takich jak StructuralPartialEq (niestabilna) podczas obniżania HIR (Wysoko-poziomowe Przedstawienie Pośrednie) i sprawdzania typów. Napotykając parametr ogólny const, kompilator sprawdza, czy typ jest liczbą całkowitą, bool lub char, lub typem zdefiniowanym przez użytkownika, wyraźnie oznaczonym jako wspierający równość strukturalną. Odrzuca typy zmiennoprzecinkowe, ponieważ ich równość nie jest refleksywna, a także odrzuca referencje takie jak &str, ponieważ wprowadzają złożoność żywotności i pośrednictwa, które nie mogą być uzgodnione w kontekście 'static wymaganym dla ogólnych const. Podczas monomorfizacji kompilator ocenia wyrażenia const i używa równości strukturalnej do łączenia identycznych instancji, zapewniając bezpieczeństwo typów.
// Prawidłowy: usize ma równość strukturalną struct Matrix<const N: usize> { data: [[f64; N]; N], } // Nieprawidłowy: f64 brakuje pełnego porządku (problemy z NaN) // struct Physics<const G: f64>; // Błąd: typy zmiennoprzecinkowe nie mogą być używane w ogólnych const // Nieprawidłowy: &str ma pośrednictwo i złożoność żywotności // struct Label<const S: &str>; // Błąd: `&str` jest zabroniony jako typ parametru ogólnego const
Projektujesz silnik handlu o wysokiej częstotliwości, w którym instrumenty finansowe muszą nosić stałe parametry kompilacji dla specyfikacji kontraktów, takie jak rozmiary ticków (np. 0.25 USD) lub współczynniki mnożnika. Początkowy projekt próbował użyć ogólnych const f64 do bezpośredniego kodowania tych precyzyjnych wartości dziesiętnych w systemie typów, mając nadzieję na wyeliminowanie przechowywania tych stałych w czasie wykonywania i umożliwienie optymalizacji obliczeń wyceny w czasie kompilacji.
Jednym z rozważanych podejść było obejście ograniczenia poprzez transmutację bitów f64 na u64 i użycie tego jako parametru const, a następnie transmutacja z powrotem w realizacji. Okazało się jednak, że jest to niebezpieczne, ponieważ identyczne bitowo liczby zmiennoprzecinkowe mogą reprezentować różne wartości semantyczne z powodu zera ze znakiem (+0.0 vs -0.0) i ładunków NaN, co potencjalnie mogłoby spowodować, że kompilator potraktuje różne instrumenty finansowe jako ten sam typ lub połączy obliczenia, które powinny pozostać oddzielne, prowadząc do błędnej logiki cenowej.
Inne rozwiązanie polegało na użyciu stałych skojarzonych w ramach cechy (trait Instrument { const TICK_SIZE: f64; }). Chociaż pozwala to na wartości zmiennoprzecinkowe, poświęca możliwość użycia rozmiaru ticka jako dyskryminatora na poziomie typów; nie można mieć Vec<Instrument<TICK_SIZE>> zawierającego różne instrumenty z różnymi rozmiarami ticka bez uciekania się do obiektów strukturalnych dyn Trait, co wprowadza pośrednictwo vtable, nieakceptowalne w ścieżce krytycznej.
Wybrane rozwiązanie polegało na zakodowaniu wartości zmiennoprzecinkowych jako stałych liczb całkowitych (np. reprezentując 0.25 USD jako usize 25 z domyślnym współczynnikiem skalowania 100). To podejście spełnia wymagania ogólnych const, zachowując jednocześnie zerowy koszt abstrakcji i ewaluację w czasie kompilacji. Rezultatem był system kontraktów bezpieczny typowo, w którym Bond<25> i Bond<50> są różnymi typami bez obciążeń czasowych, chociaż wymagało to starannej dokumentacji konwencji skalowania, aby zapobiec błędom arytmetycznym.
Dlaczego Rust dopuszcza char i bool jako parametry ogólne const, ale wyklucza &str, biorąc pod uwagę, że oba są technicznie typami prymitywnymi?
Char i bool są typami wartości z ustaloną wielkością i trywialną równością strukturalną; char to 32-bitowa wartość skalarna Unicode, a bool to ściśle 0 lub 1, co pozwala na porównanie bitowe. &str to wskaźnik gruby (lub referencja do DST) zawierający wskaźnik do danych oraz długość, wprowadzając pośrednictwo i parametry żywotności. Kompilator nie może zagwarantować, że dwa literały łańcucha znajdują się w tym samym adresie pamięci w różnych paczkach lub że ich żywotności spełniają wymagania 'static w sposób, który pozwala na sprawdzenie tożsamości typów. W konsekwencji &str brakuje właściwości strukturalnych wymaganych dla parametrów ogólnych const, podczas gdy char i bool są wartościami samoistnymi.
Jak wdrożenie ogólnych const dla typów zmiennoprzecinkowych potencjalnie mogłoby naruszyć bezpieczeństwo typów w odniesieniu do wartości NaN (Nie-liczba)?
Gdyby f32 był dozwolony, wyrażenia takie jak MyStructf32::NAN i MyStruct<{ 0.0 / 0.0 }> wytworzyłyby obie wartości NaN, ale kompilator nie mógłby zagwarantować, że reprezentują ten sam typ, ponieważ NaN != NaN. To pozwoliłoby na stworzenie dwóch odrębnych monomorfizacji tego, co logicznie powinno być tym samym typem, lub odwrotnie, zmusiłoby kompilator do błędnego łączenia typów zawierających różne ładunki NaN. To naruszenie tożsamości typów mogłoby prowadzić do niespójności, gdzie wzorce singletonów nie działają, lub gdzie optymalizacje oparte na typach produkują błędny kod, ponieważ kompilator zakłada, że parametry typów unikalnie identyfikują pojedynczy typ.
Jaka jest fundamentalna różnica między ogólnymi const a stałymi skojarzonymi, i dlaczego pierwsze wymagają równości strukturalnej, podczas gdy drugie nie?
Parametry ogólne const są częścią tożsamości typu; Container<10> i Container<20> to różne typy z oddzielnymi monomorfizacjami. Wymaga to, aby wartości były porównywalne w czasie kompilacji, aby zapewnić globalną unikalność i połączyć identyczne instancje. Stałe skojarzone to wartości związane z implementacją typu, ale nie zmieniają samego typu; TypeA i TypeB pozostają odrębnymi typami, niezależnie od wartości ich stałych skojarzonych. Dlatego stałe skojarzone mogą być zmiennoprzecinkowe lub złożone, ponieważ po prostu dostarczają wartości w ramach implementacji, nie wpływając na sprawdzanie typów ani monomorfizację, omijając potrzebę równości strukturalnej na poziomie systemu typów.