Использование макроса для инициализации большого массива не копируемых элементов

Я пытаюсь инициализировать большой массив элементов одним и тем же инициализатором. 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

Есть ли способ инициализировать этот массив без вырезания и вставки каждого инициализатора?


person hellcatv    schedule 28.03.2016    source источник
comment
Связанный: Есть ли способ подсчета с помощью макросов, tl ;dr -› не с обычными макросами.   -  person Lukas Kalbertodt    schedule 28.03.2016


Ответы (3)


Проблема в том, что расширение макроса абсолютно должно быть полным и независимым грамматическим элементом. Вы не можете расшириться до a, b так же, как вы не можете расшириться до 42 +. В Rust также нет способа (статически) конкатенировать или конкатенировать массивы; весь инициализатор массива должен быть расширен за один шаг.

Это можно сделать с помощью макросов с накоплением push-down. . Хитрость заключается в том, что вы передаете еще не синтаксически допустимое выражение частичного массива вниз по рекурсии, вместо построения на обратном пути. Когда вы достигаете нижней части расширения, вы одновременно испускаете теперь завершенное выражение.

Вот макрос, который поддерживает массивы длиной от 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 , ] }
person DK.    schedule 28.03.2016

Проблема с этими макросами заключается в том, что первый не создает правильных синтаксических форм в 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 для некоторых массивов фиксированного размера; используя его, вы можете сделать что-то вроде этого:

use arrayvec::ArrayVec; // 0.5.1

fn main() {
    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]
}

Смотрите также:

person Vladimir Matveev    schedule 28.03.2016
comment
Это действительно крутая библиотека - к сожалению, я забыл упомянуть, что хочу воздержаться от небезопасной территории, особенно для стороннего кода. - person hellcatv; 28.03.2016
comment
На самом деле мне нужен только интерфейс списка ... Интересно, могу ли я использовать несколько макросов для создания списка размером N, может быть, какая-то навязчивая вещь с опцией next - person hellcatv; 28.03.2016
comment
@hellcatv, если вы хоть немного используете стандартную библиотеку, вы будете зависеть от небезопасного кода. Почти все полезные абстракции (коллекции, управление памятью, ввод-вывод, многопоточность и т. д.) основаны на небезопасном коде и без него не обойтись. Нет ничего плохого в использовании безопасных интерфейсов вместо небезопасных внутренностей. Вам не обязательно использовать unsafe самостоятельно. - person Vladimir Matveev; 28.03.2016

"безопасная" реализация, работающая на стабильной, интенсивно вдохновленный 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 }));
// }
person ronlobo    schedule 07.01.2020
comment
Я бы рекомендовал большинству людей не использовать этот ответ. ArrayVec, как показано в ответе Владимира Матвеева, выполняет небезопасный код под капотом и предоставляет более удобный API. - person Shepmaster; 07.01.2020