Implement Default trait on const genric array

The core library will implement Default trait for normal array types, for example:

let array = <[i32;3]>::default();

But an array represented by const generic will not:

trait Foo: Default {}

trait Bar {
    fn bar();
}

impl<T: Foo, const N: usize> Bar for [T; N] {
    fn bar() {
        let array: [T; N] = <[T; N]>::default();
    }
}

Even I declare that trait Foo must implement Default, the compiler said:

error[E0599]: no function or associated item named `default` found for array `[T; N]` in the current scope
 --> src/main.rs:9:39
  |
9 |         let array: [T; N] = <[T; N]>::default();
  |                                       ^^^^^^^ function or associated item not found in `[T; N]`

And it doesn't allow me to implement manually either:

impl<T: Foo, const N: usize> Default for [T; N] {
    fn default() -> Self {
        [T::default(); N]
    }
}

it said first all arrays are foreign, so I invalid the orphan rule, and a second error said my implementation overlaps:

conflicting implementations of trait std::default::Default for type [_; 32]

So in this case, how can I implement the Default trait for [S;N]?

You can't. And the standard library not providing such an implementation is a know limitation (which is basically due to the fact that [T; 0] has at some point or, probably accidentally, gotten a Default implementation without a T: Default bound).

You can still make your bar function work, using array::map

trait Foo: Default {}

trait Bar {
    fn bar();
}

impl<T: Foo, const N: usize> Bar for [T; N] {
    fn bar() {
        let array: [T; N] = [(); N].map(|_| T::default());
    }
}

Rust Playground

2 Likes

I noticed that the core library implemented arrays which size is 32, do you know where is the 32 comes from? Is this the same reason because of the limitation you mentioned?

In my case, I need to call T: default on any type, for example:

fn is_default<T: Default>(v: &T) -> bool {
    v == T::default()
}

Since I can't implement Default for [T; N], is there any way I can still make my function work?

P.S. I also tried this:

fn is_default<T: Default, const N: usize>(v: &[T;N]) -> bool {
    
}

but it doesn't work because Rust do not support function overriding (or partial specialization maybe?)

Back when those implementations were written in Rust 1.4.0, there was no such thing as const generics so there needed to be an explicit Default implementation for arrays of each size.

The std authors used a macro to automate the boilerplate, but because it isn't practical to generate an impl<T: Default> Default for [T; N] block for each N from 0 to infinity, they chose to stop at a "good enough" number... 32.

1 Like

It looks to me that since const generic becomes stable, the std lib could update the implementation with const generic, and if so, my case will also be solved, right?

I don't know.

The standard library can't have both impl<T> Default for [T; 0] and impl<T: Default, const N: usize> Default [T; N] because one isn't an exact superset of the other (the condition for specialisation).

You also can't remove the [T; 0] impl because it will break the code of anyone using <[T; 0]>::default() where T: !Default. That also assumes removing such an impl is allowed... as I understand it, editions don't let you remove trait implementations because std types from one edition of Rust must be usable with code written against a different edition.

Seems like they might have painted themselves into a corner :man_shrugging:

1 Like

I am still not very clear why [T;0] so special? Although @steffahn mentioned above, but I didn't get it :joy:

For example the code below compiles fine today.

let arr: [TcpStream; 0] = Default::default();

And we can't have both const generic Default impl and the compatibility with the code above at this time.

1 Like

According to this conversation, it seems that allowing array size to be 0 does more disadvantages than advantages, right?

Empty arrays are useful, though. Forbidding zero-length arrays would mean you can't write &[] any more.

2 Likes

I understand somehow:

trait Foo {}

fn default<T: Foo>() {
    let a : [T;0] = <[T;0]>::default();
}

this will compile even though Foo is not a sub trait of default, but this will not:

fn default<T: Foo>() {
    let a : [T;1] = <[T;1]>::default();
}

If I understand right, this feature does not use trait implementation, but somehow hard code in compilers, that why it conflict with impl Default for [T;N];, right?

No, it is an explicit impl in std.

2 Likes

I don't know whether it was deliberate or an accident, but it's definitely useful.

I think we need some (very limited!) way to specify impl priorities, at least for std, but I haven't found such good way until now.

The best solution I have is to mark an impl as #[trivial] or something when a #[trivial] impl can conflict with other impls; that will also solve the questions raised from time to time about clashing with impl<T> From<T> for T.

But it may have the same soundness hole as specialization, I haven't checked that.

There have been a few attempts, perhaps this is the most recent, but it's a tough nut to crack in a way that doesn't depend on questionable implementation, doesn't disrupt inference, doesn't make other teams balk, etc.

It's a goal of the const generics project group.

3 Likes

It seems that even normal usage of specialization(by using min_specialization) will not fix this, really a difficult problem than it looks...

Even the full specialization won't.

So I think focusing on letting N > 1 rather than using specialization is the right way to go, just don't know when will the grammar finally be landed...

Because you can create an empty array of absolutely any type whatsoever, out of thin air.

This works because creating an empty array doesn't involve creating any elements – in fact, creating an empty array basically doesn't involve… pretty much anything. It needs no memory to allocate, no values to move/copy, nothing.

(The only case I can imagine involving some actual work is when an empty array is coerced to a slice reference, in which case an appropriately aligned pointer must be created. But that's just a constant equal to align_of::<T>() anyway.)

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.