You can use the arrayvec crate which allows creation of owned iterators without heap allocations. Alternately itertools provides a cartesian_product. Basically the issue is that you want to move the integers but not the arrays, which you should also be able to do, but I wasn't able to get it to work in the short time I have before going to bed.
let xs = 0..4;
let ys = 0..2;
let cross = ys.flat_map(|y| xs.clone().map(move |x| (x, y)));
This is nice and elegant, requiring clone of the inner iterator only because its sequence gets repeated. But integers are a copy type, and copy types are easy. Let's use shared references to a move type instead:
// The input collections contain custom move types:
let xs = vec![Size2(1, 1), Size2(2, 2)];
let ys = vec![Size2(3, 3), Size2(4, 4)];
// But let's shadow them and work with iterators only.
let xs = xs.iter();
let ys = ys.iter();
let cross = ys.flat_map(|y| xs.clone().map(move |x| (x, y)));
Cool, the same expression works here too, producing pairs of references. Maybe we're done, or maybe we want values instead:
let cross = ys.flat_map(|y| xs.clone().map(move |x| (x.clone(), y.clone())));
The key here is to work with iterators over the collections, not the collections themselves. Only when producing final outputs do we clone the elements, because naturally they get reused across the product.
At first, I thought of X * Y * Z as (X * Y) * Z and then it's easy to work sequentially:
let xys = ys.flat_map(|y| xs.clone().map(move |x| (x, y)));
let xyzs = zs.flat_map(|z| xys.clone().map(move |(x, y)| (x, y, z)));
This is okay for light copy types. But when using move types, it's more efficient to save cloning for the very end, so setting up the borrow is worth a little more complexity.
@joonazan That's a nice find about the capturing technique. The equivalent for what I originally wrote would be:
zs.flat_map(|z| { let xs = &xs; ys.clone().flat_map(move |y| xs.clone().map(move |x| (x, y, z))) } );
And for move types just replace the final value (x, y, z) with (x.clone(), y.clone(), z.clone()).
But it's even easier if you set up borrows to the iterators and use only move closures, because shared refs are copy types:
let xs = &xs;
let ys = &ys;
let xyzs = zs.flat_map(move |z| ys.clone().flat_map(move |y| xs.clone().map(move |x| (x, y, z))));
C++ captures are concise and powerful but a little dangerous and tricky for newcomers. In Rust I might actually prefer it if all closures were implicitly move, and borrows had to be set up by creating reference values. This would be consistent with normal function calls where parameter values (including references) always move into the function.