История: Константные обобщения были стабилизированы в Rust 1.51, чтобы разрешить параметризацию типов константными значениями простых целых типов, что позволяет создавать обобщенные массивы фиксированного размера, такие как [T; N]. На этапе проектирования команда разработчиков явно ограничила параметры константных обобщений типами, обладающими структурным равенством и детерминированной компиляцией. Это ограничение исключило f32, f64 и литералы &str из-за их нарушения тотального порядка или зависимости от адресов памяти во время выполнения.
Проблема: Основная проблема с числами с плавающей точкой заключается в наличии NaN (Не-Число), что нарушает рефлексивное равенство (NaN != NaN), что не позволяет компилятору надежно определять идентичность типов во время мономорфизации. Для строковых литералов (&str) проблема заключается в их представлении «широкого указателя» (адрес + длина) и их зависимости от конкретных адресов памяти в сегменте данных, которые не являются детерминированными для разных единиц компиляции или крейтов. Типовая система требует, чтобы MyStruct<1> и MyStruct<1> всегда относились к одному и тому же типу, требуя, чтобы равенство параметра константы можно было решить путем побитового или структурного сравнения во время компиляции.
Решение: Компилятор Rust реализует эти ограничения через внутренние трейты, такие как StructuralPartialEq (недоступно), во время снижения HIR (высокоуровенное промежуточное представление) и проверки типов. При встрече с параметром константного обобщения компилятор проверяет, что тип является целым числом, bool или char, или пользовательским типом, явно помеченным как поддерживающим структурное равенство. Он отклоняет числа с плавающей точкой, поскольку их равенство не является рефлексивным, и отклоняет ссылки, такие как &str, поскольку они вводят жизни и индирекцию, которые нельзя согласовать в контексте 'static, необходимом для константных обобщений. Во время мономорфизации компилятор оценивает константные выражения и использует структурное равенство для объединения идентичных инстанциаций, обеспечивая безопасность типов.
// Допустимо: usize имеет структурное равенство struct Matrix<const N: usize> { data: [[f64; N]; N], } // Недопустимо: f64 не имеет тотального порядка (проблемы с NaN) // struct Physics<const G: f64>; // Ошибка: типы с плавающей точкой не могут использоваться в константных обобщениях // Недопустимо: &str имеет сложность индирекции и длительности жизни // struct Label<const S: &str>; // Ошибка: `&str` запрещен в качестве типа параметра константного обобщения
Вы разрабатываете движок высокочастотной торговли, где финансовые инструменты должны иметь параметры констант на этапе компиляции для спецификаций контракта, такие как размеры тиков (например, 0.25 USD) или коэффициенты множителя. Исходный дизайн пытался использовать f64 для константных обобщений, чтобы закодировать эти точные десятичные значения непосредственно в типовой системе, надеясь исключить хранение этих констант во время выполнения и обеспечить оптимизацию расчетов цен на этапе компиляции.
Один из рассматриваемых подходов заключался в обходе ограничения, преобразуя f64 в u64 и используя это в качестве параметра константы, а затем преобразуя обратно в реализации. Однако это оказалось рискованным, поскольку побитовые идентичные числа с плавающей точкой могут представлять различные семантические значения из-за знакового нуля (+0.0 против -0.0) и полезных нагрузок NaN, что потенциально может заставить компилятор воспринимать различные финансовые инструменты как один и тот же тип или объединять расчеты, которые должны оставаться отдельными, что приводит к неправильной логике цен.
Другое решение состояло в использовании связанных констант внутри трейта (trait Instrument { const TICK_SIZE: f64; }). Хотя это позволяет использовать числа с плавающей точкой, это жертвует возможностью использовать размер тика в качестве дискриминатора на уровне типов; вы не можете иметь Vec<Instrument<TICK_SIZE>> с разными инструментами с различными размерами тиков, не прибегая к накладным расходам объектов dyn Trait, что вводит индирекцию виртуальной таблицы, недопустимую в горячем пути.
Выбранным решением было кодировать значения с плавающей точкой в фиксированные целые числа (например, представление 0.25 USD как usize 25 с неявным коэффициентом масштабирования 100). Этот подход соответствует ограничениям константных обобщений при поддержании нулевой стоимости абстракции и оценки на этапе компиляции. В результате получилась безопасная с точки зрения типов система контрактов, где Bond<25> и Bond<50> являются различными типами без накладных расходов во время выполнения, хотя это потребовало внимательной документации масштаба, чтобы предотвратить арифметические ошибки.
Почему Rust допускает char и bool в качестве параметров константного обобщения, но исключает &str, учитывая, что оба они технически являются примитивными типами?
Char и bool являются типами значений фиксированного размера и тривиального структурного равенства; char является 32-битным юникодовым скалярным значением, а bool строго равен 0 или 1, что позволяет побитовое сравнение. &str является широким указателем (или ссылкой на DST), содержащим указатель данных и длину, что вводит индирекцию и параметры жизни. Компилятор не может гарантировать, что два строковых литерала находятся по одному и тому же адресу памяти в разных крейтах или что их жизни удовлетворяют требованиям 'static таким образом, чтобы позволить проверку идентичности типов. Следовательно, &str не обладает структурными свойствами, необходимыми для параметров константного обобщения, в то время как char и bool являются самодостаточными значениями.
Как реализация константных обобщений для типов с плавающей точкой потенциально может нарушить безопасность типов в отношении значений NaN (Не-Число)?
Если f32 было бы разрешено, такие выражения, как MyStructf32::NAN и MyStruct<{ 0.0 / 0.0 }> оба бы производили значения NaN, но компилятор не смог бы гарантировать, что они представляют один и тот же тип, поскольку NaN != NaN. Это позволило бы создать две различные мономорфизации того, что логически должно быть одним и тем же типом, или, наоборот, заставить компилятор неправильно объединить типы, которые содержат разные полезные нагрузки NaN. Это нарушение идентичности типов может привести к ненадежности, когда паттерны одиночек терпят неудачу или когда оптимизации на основе типов выдают неправильный код, так как компилятор предполагает, что параметры типов уникально идентифицируют один тип.
Каково фундаментальное различие между константными обобщениями и связанными константами, и почему первые требуют структурного равенства, а последние — нет?
Параметры константного обобщения являются частью идентичности типа; Container<10> и Container<20> — это разные типы с отдельными мономорфизациями. Это требует, чтобы значения можно было сравнивать во время компиляции, чтобы гарантировать глобальную уникальность и объединять идентичные инстанциации. Связанные константы — это значения, ассоциированные с реализацией типа, но не изменяющие сам тип; TypeA и TypeB остаются различными типами независимо от их связанных значений. Поэтому связанные константы могут быть числами с плавающей точкой или сложными типами, так как они просто предоставляют значения в реализации, не влияя на проверку типов или мономорфизацию, обходя необходимость структурного равенства на уровне типовой системы.