Initialize a large, fixed-size array with non-Copy types
Asked Answered
J

8

66

I’m trying to initialize a fixed-size array of some nullable, non-copyable type, like an Option<Box<Thing>> for some kind of Thing. I’d like to pack two of them into a struct without any extra indirection. I’d like to write something like this:

let array: [Option<Box<Thing>>; SIZE] = [None; SIZE];

But it doesn’t work because the [e; n] syntax requires that e implements Copy. Of course, I could expand it into SIZE Nones, but that can be unwieldy when SIZE is large. I don’t believe this can be done with a macro without an unnatural encoding of SIZE. Is there a good way to do it?

Yes, this is easy with unsafe; is there a way to do it without unsafe?

Jenna answered 22/2, 2015 at 9:33 Comment(2)
specifically for [Option<Box<T>>; N] you can use transmute from a [0; N]: is.gd/CC31YQHydrodynamics
I wrote an answer to a similar question #36926173Bathtub
F
53

As of Rust 1.79 (to be released in June 2024), a simple way to do this is using inline const:

struct Thing;

fn main() {
    const SIZE: usize = 100;
    let _array: [Option<Box<Thing>>; SIZE] = [const {None}; SIZE];
}

Playground

That also works for initializing static variables.

As of Rust 1.63 (released in August 2022), a cleaner alternative to previously posted answers is possible using std::array::from_fn():

const SIZE: usize = 100;
let array: [Option<Box<Thing>>; SIZE] = std::array::from_fn(|_| None);

Playground

If you need to support Rust versions older than 1.63, or if you need to initialize a static, an alternative approach using an intermediate const initializer works as of Rust 1.38 (released in September 2019):

const SIZE: usize = 100;
const INIT: Option<Box<Thing>> = None; // helper
// also works with static array
let array: [Option<Box<Thing>>; SIZE] = [INIT; SIZE]; 

Playground

The first and the last approach have the limitation that the array item must have a representation that can be evaluated at compile time - a constant, enum variant, empty Vec or String, or a primitive container (enum, tuple) composed of those. None or a tuple of numbers will work, but a non-empty Vec or String won't. The array::from_fn() approach has no such limitation.

All the above examples work with or without the Box; they use Box because that was used in the question. All work for arrays of any size.

Fiorenze answered 24/3, 2021 at 7:32 Comment(0)
P
32

You could use the Default trait to initialize the array with default values:

let array: [Option<Box<Thing>>; SIZE] = Default::default();

See this playground for a working example.

Note that this will only work for arrays with up to 32 elements, because Default::default is only implemented for up to [T; 32]. See https://doc.rust-lang.org/std/default/trait.Default.html#impl-Default-for-%5BT%3B%2032%5D.

Poacher answered 6/12, 2016 at 13:0 Comment(9)
Unfortunately this doesn't work if SIZE is a generic parameter since that parameter can exceed 32.Camire
@Camire Exactly. See the answer https://mcmap.net/q/295439/-initialize-a-large-fixed-size-array-with-non-copy-types for a more recent solution which also works with generic parameters. See also play.rust-lang.org/…Poacher
@Poacher even the answer you link to fails for me with generic parameters: E0401 > can't use generic parameters from outer functionAlmonry
@AndrewArnott it's difficult to know what your problem is without code. Best to create an example on play.rust-lang.org and link it.Poacher
@rnstlr, here you go: play.rust-lang.org/…Georgettageorgette
@Georgettageorgette Ah I see. We can get around it by defining the INIT value as an associated constant: play.rust-lang.org/…Poacher
@Poacher nope, doesn't work. sadly you can't use generics in const.Georgettageorgette
@Georgettageorgette So what doesn't work? With the code in the second playground I posted we can create an array any Option<T> initialized with None.Poacher
@rnstlr, yeah, I've missed the part about associated constantGeorgettageorgette
W
29

As of Rust 1.55.0 (which introduced <[T; N]>::map()), the following will work:

const SIZE: usize = 100;

#[derive(Debug)]
struct THING { data: i64 }

let array = [(); SIZE].map(|_| Option::<THING>::default());
for x in array {
    println!("x: {:?}", x);
}

Rust Playground

Winthorpe answered 28/10, 2021 at 15:11 Comment(4)
Using the map method comes with some performance considerations, though. The documentation states: "Unfortunately, usages of this method are currently not always optimized as well as they could be. This mainly concerns large arrays, as mapping over small arrays seem to be optimized just fine. Also note that in debug mode (i.e. without any optimizations), this method can use a lot of stack space (a few times the size of the array or more)."Appellate
@AlejandroGonzález does it refer to the size of the input or output array? the input array is just ZST.Predate
@Predate [T]::map() is defined to return an array of the same size as the input, so there is only one size to care about.Appellate
Scrap that, it refers to the array length! Honestly, it's a good question I don't know the answer for.Appellate
C
7

I'm copying the answer by chris-morgan and adapting it to match the question better, to follow the recommendation by dbaupp downthread, and to match recent syntax changes:

use std::mem;
use std::ptr;

#[derive(Debug)]
struct Thing {
    number: usize,
}

macro_rules! make_array {
    ($n:expr, $constructor:expr) => {{
        let mut items: [_; $n] = mem::uninitialized();
        for (i, place) in items.iter_mut().enumerate() {
            ptr::write(place, $constructor(i));
        }
        items
    }}
}

const SIZE: usize = 50;

fn main() {
    let items = unsafe { make_array!(SIZE, |i| Box::new(Some(Thing { number: i }))) };
    println!("{:?}", &items[..]);
}

Note the need to use unsafe here: The problem is that if the constructor function panic!s, this would lead to undefined behavior.

Cortisol answered 23/2, 2015 at 3:24 Comment(1)
Note that this solution doesn't allow initializing static (global) arrays. The question didn't specify if that's actually needed, but it might be relevant to future readers.Fiorenze
E
3

Go through the heap

If you can create a Vec of your type, you can convert it into an array:

use std::convert::TryInto;

#[derive(Clone)]
struct Thing;
const SIZE: usize = 100;

fn main() {
    let v: Vec<Option<Thing>> = vec![None; SIZE];
    let v: Box<[Option<Thing>; SIZE]> = match v.into_boxed_slice().try_into() {
        Ok(v) => v,
        Err(_) => unreachable!(),
    };
    let v: [Option<Thing>; SIZE] = *v;
}

In many cases, you actually want to leave it as a Vec<T>, Box<[T]>, or Box<[T; N]> as these types all put the data in the heap. Large arrays tend to be... large... and you don't want all that data on the stack.

See also:

Keep it simple

Type out all the values:

struct Thing;
const SIZE: usize = 5;

fn main() {
    let array: [Option<Box<Thing>>; SIZE] = [None, None, None, None, None];
}

You could use a build script to generate this code for you. For an example of this, see:

Embarkation answered 28/1, 2019 at 22:3 Comment(4)
If n = 256 should I really just copy & paste None 256 times?Clower
@MatejKormuth depends on a lot of factors, but I don't see anything inherently wrong with it. It's annoying, yes, but simple.Embarkation
One such factor: it does work if SIZE is genericCamire
Was looking for Keep it simple advice, my case array is always 20x20=400 elements. Now I'm looking into how to write a macro which would expand into 400 Nones.Leisurely
C
2

In Rust 1.79.0, scheduled for release in 13 June, 2024, you can use inline const. This is a variant of the answer by @user4815162342, but one that doesn't require you to declare a separate constant and repeat the type:

let array: [Option<Box<Thing>>; SIZE] = [const { None }; SIZE];

Until this is stabilized, you can also use the inline-const crate, but this does require you to repeat the type.

Cowper answered 27/12, 2022 at 16:55 Comment(0)
F
1

An alternative approach using the arrayvec crate that generalizes easily to situations other than initializing everything with a fixed value:

use arrayvec::ArrayVec;

let array = std::iter::repeat(None)
    .take(SIZE)
    .collect::<ArrayVec<Option<Box<Thing>>, SIZE>>()
    .into_inner()
    .unwrap();

(playground)

Floristic answered 5/6, 2021 at 7:21 Comment(0)
K
1

As of Rust 1.63, you can use from_fn:

let arr: [Option<Box<i64>>; 1000] = std::array::from_fn(|_| None);
assert!(arr.iter().all(|item| item.is_none()));

playground

Kurd answered 26/9, 2022 at 10:14 Comment(1)
Please don't post code-only answers. The main audience, future readers, will be grateful to see explained why this answers the question instead of having to infer it from the code. Also, since this is an old, well answered question, please explain how it complements all other answers.Litre

© 2022 - 2024 — McMap. All rights reserved.