Использование макроса для инициализации большого массива элементов без копирования
Я пытаюсь инициализировать большой массив элементов одним и тем же инициализатором. 64 элемента - это просто пример - я хочу сделать его как минимум 16 КБ. К сожалению простой
let array : [AllocatedMemory<u8>; 64] = [AllocatedMemory::<u8>{mem:&mut []};64];
не будет работать, потому что AllocatedMemory
структура не реализует Copy
error: the trait `core::marker::Copy` is not implemented for the type `AllocatedMemory<'_, u8>` [E0277]
let array : [AllocatedMemory<u8>; 64] = [AllocatedMemory::<u8>{mem:&mut []}; 64];
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Поэтому я попробовал макросы безрезультатно:
struct AllocatedMemory<'a, T: 'a> {
mem: &'a mut [T],
}
macro_rules! init_memory_helper {
(1, $T : ty) => { AllocatedMemory::<$T>{mem: &mut []} };
(2, $T : ty) => { init_memory_helper!(1, $T), init_memory_helper!(1, $T) };
(4, $T : ty) => { init_memory_helper!(2, $T), init_memory_helper!(2, $T) };
(8, $T : ty) => { init_memory_helper!(4, $T), init_memory_helper!(4, $T) };
(16, $T : ty) => { init_memory_helper!(8, $T), init_memory_helper!(8, $T) };
(32, $T : ty) => { init_memory_helper!(16, $T), init_memory_helper!(16, $T) };
(64, $T : ty) => { init_memory_helper!(32, $T), init_memory_helper!(32, $T) };
}
macro_rules! init_memory {
(1, $T : ty) => { [init_memory_helper!(1, $T)] };
(2, $T : ty) => { [init_memory_helper!(2, $T)] };
(4, $T : ty) => { [init_memory_helper!(4, $T)] };
(8, $T : ty) => { [init_memory_helper!(8, $T)] };
(16, $T : ty) => { [init_memory_helper!(16, $T)] };
(32, $T : ty) => { [init_memory_helper!(32, $T)] };
(64, $T : ty) => { [init_memory_helper!(64, $T)] };
}
fn main() {
let array: [AllocatedMemory<u8>; 64] = init_memory!(64, u8);
println!("{:?}", array[0].mem.len());
}
Сообщение об ошибке
error: macro expansion ignores token `,` and any following
(64, $T : ty) => { init_memory_helper!(32, $T), init_memory_helper!(32, $T) };
note: caused by the macro expansion here; the usage of `init_memory_helper!` is likely invalid in expression context
Есть ли способ инициализировать этот массив без вырезания и вставки каждого инициализатора?
2 ответа
Проблема заключается в том, что расширение макроса обязательно должно быть полным и независимо действительным грамматическим элементом. Вы не можете расширить до a, b
больше, чем вы можете расширить 42 +
, В Rust также нет способа (статически) объединять или использовать минусы; весь инициализатор массива должен быть расширен за один шаг.
Это можно сделать с помощью макросов с отложенным накоплением. Хитрость заключается в том, что вы передаете еще не синтаксически верное выражение частичного массива по рекурсии, вместо того, чтобы строить на обратном пути. Когда вы достигнете нижней части раскрытия, вы сразу выдадите законченное выражение.
Вот макрос, который поддерживает массивы длиной от 0 до 8 и степенями от 2 до 64:
macro_rules! array {
(@accum (0, $($_es:expr),*) -> ($($body:tt)*))
=> {array!(@as_expr [$($body)*])};
(@accum (1, $($es:expr),*) -> ($($body:tt)*))
=> {array!(@accum (0, $($es),*) -> ($($body)* $($es,)*))};
(@accum (2, $($es:expr),*) -> ($($body:tt)*))
=> {array!(@accum (0, $($es),*) -> ($($body)* $($es,)* $($es,)*))};
(@accum (3, $($es:expr),*) -> ($($body:tt)*))
=> {array!(@accum (2, $($es),*) -> ($($body)* $($es,)*))};
(@accum (4, $($es:expr),*) -> ($($body:tt)*))
=> {array!(@accum (2, $($es,)* $($es),*) -> ($($body)*))};
(@accum (5, $($es:expr),*) -> ($($body:tt)*))
=> {array!(@accum (4, $($es),*) -> ($($body)* $($es,)*))};
(@accum (6, $($es:expr),*) -> ($($body:tt)*))
=> {array!(@accum (4, $($es),*) -> ($($body)* $($es,)* $($es,)*))};
(@accum (7, $($es:expr),*) -> ($($body:tt)*))
=> {array!(@accum (4, $($es),*) -> ($($body)* $($es,)* $($es,)* $($es,)*))};
(@accum (8, $($es:expr),*) -> ($($body:tt)*))
=> {array!(@accum (4, $($es,)* $($es),*) -> ($($body)*))};
(@accum (16, $($es:expr),*) -> ($($body:tt)*))
=> {array!(@accum (8, $($es,)* $($es),*) -> ($($body)*))};
(@accum (32, $($es:expr),*) -> ($($body:tt)*))
=> {array!(@accum (16, $($es,)* $($es),*) -> ($($body)*))};
(@accum (64, $($es:expr),*) -> ($($body:tt)*))
=> {array!(@accum (32, $($es,)* $($es),*) -> ($($body)*))};
(@as_expr $e:expr) => {$e};
[$e:expr; $n:tt] => { array!(@accum ($n, $e) -> ()) };
}
fn main() {
let ones: [i32; 64] = array![1; 64];
println!("{:?}", &ones[..]);
}
Стратегия здесь заключается в том, чтобы умножить размер входных данных на степени двух и добавить остаток для не степеней двух. Это сделано для того, чтобы предотвратить ограничение рекурсии макроса (я считаю, что по умолчанию установлено значение 64), убедившись, что $n
падает в цене быстро.
Просто чтобы предотвратить частый вопрос: нет, вы не можете упростить это с помощью арифметики; Вы не можете делать арифметику в макросах.:)
Приложение: Если вы не уверены, как это работает, вы можете передать -Z trace-macros
в rustc
при компиляции и видеть каждый вызов макроса, который раскрывается. С помощью array![1; 6]
В качестве примера вы получите что-то вроде этого:
array! { 1 ; 6 }
array! { @ accum ( 6 , 1 ) -> ( ) }
array! { @ accum ( 4 , 1 ) -> ( 1 , 1 , ) }
array! { @ accum ( 2 , 1 , 1 ) -> ( 1 , 1 , ) }
array! { @ accum ( 0 , 1 , 1 ) -> ( 1 , 1 , 1 , 1 , 1 , 1 , ) }
array! { @ as_expr [ 1 , 1 , 1 , 1 , 1 , 1 , ] }
Проблема с этими макросами заключается в том, что первый не создает допустимых синтаксических форм в Rust - два выражения, объединенные запятой, сами по себе не являются допустимой формой. Тот факт, что он вставлен в квадратные скобки в другом макросе, не имеет значения.
Честно говоря, я не знаю, как сделать это правильно с обычными массивами. Отсутствие чисел в качестве общих параметров является хорошо известной проблемой, которая исключает множество полезных шаблонов. Например, если бы их поддерживали, можно было бы иметь такую функцию:
fn make_array<T, N: usize, F>(f: F) -> [T; N] where F: FnMut() -> T
который создает массив произвольного размера, заполняя его результатом вызова функции:
let array: [_; 64] = make_array(|| AllocatedMemory::<u8>{ mem: &mut [] })
Но, увы, в Rust пока ничего такого нет. Вы должны использовать динамические структуры, такие как Vec
вместо. Вы также можете попробовать arrayvec, который обеспечивает Vec
-подобный API для некоторых массивов фиксированного размера; используя его, вы можете сделать что-то вроде этого:
let mut array = ArrayVec::<[_; 64]>::new();
for _ in 0..array.len() {
array.push(AllocatedMemory::<u8>{ mem: &mut [] });
}
let array = array.into_inner(); // array: [AllocatedMemory<u8>; 64]
"Безопасный" реализация, который работает на конюшне, сильно вдохновлен Reddit:
// #![feature(core_intrinsics)]
// use std::ptr;
use std::mem;
use std::mem::MaybeUninit;
type MyStructValue = Vec<usize>;
type UsizeToVecBuilder = Box<dyn Fn(usize) -> Vec<usize>>;
#[derive(Debug)]
struct MyStruct {
value: MyStructValue,
}
macro_rules! make_array {
([$t:ident; $n:expr], $constructor:expr, $builder:expr) => {{
let mut data: [MaybeUninit<$t>; $n] = unsafe { MaybeUninit::uninit().assume_init() };
let mut i: usize = 0;
for elem in &mut data[..] {
*elem = MaybeUninit::new($constructor(i, $builder));
i += 1;
}
unsafe { mem::transmute::<_, [$t; $n]>(data) }
}};
}
fn main() {
println!(
"{:?}",
make_array!(
[MyStruct; 5],
|i, b: UsizeToVecBuilder| MyStruct { value: b(i) },
Box::new(|i| (0..i + 1).collect())
)
);
}
// unstable version: (see reddit: https://www.reddit.com/r/rust/comments/29ymbx/a_macro_to_fill_a_fixed_length_array/)
//
// macro_rules! make_array {
// ($n:expr, $constructor:expr) => {{
// let mut items: [_; $n] = unsafe { mem::uninitialized() };
// for i in 0..$n {
// let val = $constructor(i);
// unsafe {
// std::intrinsics::volatile_copy_nonoverlapping_memory(
// &mut items[i], &val, 1
// );
// // ptr::copy_nonoverlapping_memory(&mut items[i], &val, 1);
// mem::forget(val);
// }
// }
// items
// }}
// }
// fn main() {
// unstable version:
// println!("{:?}", make_array!(5, |i| MyStruct { value: i }));
// }