
Как вы знаете, в Rust массив можно инициализировать при помощи литералов двумя способами. Первый — перечислить все элементы массива через запятую внутри квадратных скобок. Второй — в квадратных скобках указать значение и после точки с запятой — длину массива: в этом случае массив будет заполнен копиями указанного значения. Так или иначе, нужно задать всё элементы массива сразу. И это не всегда бывает удобно.
В C и C++ есть такая вещь, как zero initialization. Не вдаваясь в подробности, скажу только, что она позволяет инициализировать массив, указав значения не для всех элементов. Остальные значения будут проинициализированы нулём. Что является нулём для каждого отдельного типа — вопрос отдельный, но для числовых типов это будет, собственно, ноль. Можно ли воссоздать данный функционал в Rust? Ну, как вы могли догадаться по факту наличия этого поста — можно!
Но сначала подумаем, как мы можем это организовать. Причём тут есть два вопроса: как заполнять массив и как определять нулевое значение. Решением в духе C++ было бы сделать сначала неинициализированный массив, записать в него данные значения, а потом дописать в него нули (в прямом смысле, через
std::mem::zeroed
). Такой подход страдает следующими недостатками:* нужно определить, какие типы можно безопасно инициализировать нулевыми байтами. Можно это сделать через маркерный трейт, но, во-первых, это придётся сделать unsafe трейтом, а во-вторых, это стоило бы сделать auto-трейтом, которые на стабильной версии пока что делать нельзя.
* нужно разбираться с дропом элементов в случае, если создание одного из них запаникует. Не то чтобы это было прям сложно, но это дополнительная возня и дополнительный unsafe.
Но постойте-ка, у нас же есть Rust, мы можем сделать лучше! Заполнять массив будем иначе: сначала размножим нулевое значение, а потом перепишем префикс. Да, у такого подхода есть свои недостатки, и о них обязательно упомяну. Для того, чтобы можно было использовать синтаксис
[elem; length]
, требуется, чтобы elem
было либо выражением, имеющим Copy
-тип, либо константой. Не будем излишне ограничивать наш подход и будем для нулевого значения использовать ассоциированную константу трейта. Это, в частности, позволит нам использовать "нулевые" значения типов, которые нельзя корректно проинициализировать нулевыми байтами, например, Vec
:trait ConstZero {Теперь начнём писать макрос для собственно инициализации (ну да, макрос, а вы как хотели?). Хочется, чтобы то, что мы в итоге сделали, можно было использовать и для инициализации констант. Поэтому ограничимся только операциями, которые можно применять в const-контексте. На вход будем принимать имеющиеся элементы массива и итоговую дину — то, без чего никуда:
const ZERO: Self;
}
macro_rules! arr_init {Если вы внимательно присмотритесь, то увидите, что, в отличие от обычных списков в синтаксисе Rust, тут запятая после каждого элемента обязательно. Я сделал это намеренно, чтобы в случае, если мы переписываем один элемент, синтаксис не совпадал с нативным синтаксисом для массива из повторяющихся значений, а также чтобы подчеркнуть, что пользователь макроса указывает (потенциально) не все значения массива. Перейдём к основной логике макроса:
($($elem:expr,)* ; $len:expr) => {{
// ...
}};
}
let mut ret = [<_ as ConstZero>::ZERO; $len];Использование
let mut i = 0usize;
$(
i += 1;
ret[i - 1] = $elem;
)*
ret
<_ as ConstZero>::ZERO
позволяет подтянуть нулевое значение типа, не называя его. Вот она, сила вывода типов!