Compiler error: [i32; N] doesn't have a fixed size

This silly function compiles just fine:

fn get_zeroes() -> [i32; 2] {
    let zeroes: [u32; 2] = [0; 2];
    unsafe { std::mem::transmute::<_, [i32; 2]>(zeroes) }
}

But after turning the array size into a const generic parameter, we get the following:

fn get_zeroes_alt<const N: usize>() -> [i32; N] {
    let zeroes: [u32; N] = [0; N];
    unsafe { std::mem::transmute::<_, [i32; N]>(zeroes) }
}

error[E0512]: cannot transmute between types of different sizes, or dependently-sized types
--> src/main.rs:18:14
|
18 | unsafe { std::mem::transmute::<_, [i32; N]>(zeroes) }
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: source type: [u32; N] (this type does not have a fixed size)
= note: target type: [i32; N] (this type does not have a fixed size)

According to the reference, Rust should allow N "as a parameter to any type used in the body of any functions in the item". Have I run into a compiler bug?

Yes and no. It sounds silly if you look on this trivial case, but to handle such use-cases in general one would have to allow arithmetic on const arguments (would [[u8; N]; 4] have the same size as [i32; N], hmm?). And these are explicitly excluded from const generics MVP. And since we still don't have full-blown const generics, just MVP… don't expect fixes any time soon!

You best bet is to copy these arrays piece-by-piece and then hoping that optimizer would merge everything, I'm afaraid.

2 Likes

You can do these: Rust Playground

fn get_zeroes_alt<const N: usize>() -> [i32; N] {
    let zeroes: [u32; N] = [0; N];
    zeroes.map(|x| x as i32)
}

fn get_zeroes_alt2<const N: usize>() -> [i32; N] {
    let zeroes: [u32; N] = [0; N];
    bytemuck::cast(zeroes)
}
3 Likes

You can also use transmute_copy, which handles const generics better, if you want to go down that path. I think the copy may be optimized out if you are lucky. You will have to check for your case.

Thanks for the answers! Looking forward to progress in this area. I'm used to C++ where constexpr "just works" in similar cases.

I ended up using transmute_copy(), which was perfect for my actual use case (initializing an array from MaybeUninit references).

This is basically the same distinction as how C++ does two-phase name lookup in templates vs rust where it wants a trait so that a declaration can resolve it.

There's pros and cons to both, because of course the constexpr version will also compile the definition if you do something like [i32; N] to [u8; 5 * N] (and then fail whenever you try to use it for something other than N == 0 if you're lucky, or more likely just be UB).

4 Likes

I wouldn't frame this in pros and cons. While Rust const generics are currently immature, there's no fundamental reason to prevent more complex constant expressions in a future version.

By "just works" in C++, I mean something like:

template <size_t N>
std::array<int32_t, N> get_zeroes() {
    std::array<uint32_t, N> zeroes = {};
    return std::bit_cast<std::array<int32_t, N>>(zeroes);
}

You could even evaluate get_zeroes() at compile time if it's declared constexpr. Note that std::bit_cast requires both types to have same size and be trivially copyable, so it would refuse to compile something like [i32; N] to [u8; 5 * N].

And what I mean is that this also compiles just fine:

template <size_t N>
std::array<int32_t, N> get_zeroes() {
    std::array<uint32_t, N> zeroes = {};
    return std::bit_cast<std::array<int32_t, N+1>>(zeroes);
    //                                   LOOK ^^
}

Proof: https://cpp.godbolt.org/z/K5xfYj58T

It'll always fail when you try to instantiate it, but the template itself compiles without any complaints. Rust has generally aspired to not have such issues.

(Not that is has none of them, and exactly when that's ok is still a complicated conversation.)

1 Like

Correct, there are tradeoffs between strict vs. duck typing for generic types. In my example, both languages prevent shooting yourself in the foot. Rust just takes a more principled approach (which C++ can't quite match even with concepts).

But this principled approach isn't in conflict with compile time evaluation. E.g. given T: Sized, it would be reasonable for Rust to allow let bytes = [0 as u8; std::mem::size_of::<T>()];.

Rust makes that conversation really hard by insisting that there should one perfect answer to that.

While in reality there are at least two: one for “open-ended” generics and one for “closed set” generics.

This forum (and all other Rust-related forums) are filled with questions “how can I create a generic that only accepts u8 and u16?”… with two usual, quite unsatisfactory, answers:

  1. Learn to tolerate the pain, it's for your own good!
  2. Use macros, that way you can instantiate 100500 instantiations of generic algorithm including 1050 ones that you actually need.

Both answers sound worse to me than what C++ give use: simple generics with duck-typing, but still fully compile-time checked during instantiation.

I have learned to use empty const to simulate duck-tuping in Rust to some degree and now pain is less acute, but I still have no idea why Rust developers insist that everyone should feel that pain.

1 Like

The difference is "who". With duck typing a library author might miss a function which compiles but cannot be instantiated without errors, while strict typing guarantees you that cannot happen.

This is a different problem than the one discussed above though and should be solvable much more easily. In comparison, equality between arithmetic expressions with free variables (which is essentially the initial problem) is undecidable.

1 Like

The general argument is that it's better to preload some pain on the generic API's author than cause obscure compilation errors for downstream users. Note that the call chain between a function with duck-typed generics and the final call site can be arbitrarily long, passing through multiple crates. It would be extremely frustrating if you got some obscure error message about failed type inference due to unstated assumptions on the generic function that you called, which come from some other function in some other crate, which calls some duck-typed function in a crate you never even heard of. It completely breaks the modularity and ecosystem stability, which are Rust's actual killer features.

That said, the use case for duck-typed generics is also strong. There are plenty of cases where a generic function is immediately called, and the precise trait & const generic bounds would be nothing but complicated verbose boilerplate. In those cases, the competition isn't really between statically and duck-typed generics, but rather between duck-typed generics and macros. Personally I'll take C++ templates, with all of their confusing error messages, over macros any day of the week.

In my view, the primary restriction must be that duck typing is strictly opt-in, in the most restrictive sense (e.g. specific generic parameters can be partially duck-typed, while still obeying some explicit trait bounds, and with other parameters being ordinary generics). This would mean that one could use the power of templates where needed, but would never write one accidentally, without at least some though about the consequences. Like unsafe, using template magic would stick out and invite a properly typed approach, if feasible.

There are some discussions [1][2] about macro-like functions, which would combine the best parts of generics with some macro-like (or template-like) functionality, but I'm not aware of any serious proposals in that regard. Trying to add a feature like that to the language would likely be an uphill battle and a huge implementation effort, so I feel reluctant about formally proposing something like that, even though I think it would be a good addition. Much simpler features are stuck in the nightly limbo.

2 Likes

This is a little tangential, but I found it really interesting that with Carbon they chose to provide generics like Rust's but with specialization and opt-in automatic type erasure, and they made templates an optional feature for compatibility with C++. From the description it sounds like they learned a lot from Rust generics and tried to improve on it.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.