`impl Default` for generically sized arrays? (`[T; N]`)

In my opinion it would be excellent if generically sized arrays like [T; N] could have a default implementation. Something like:

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

would be really helpful.

Well, just found the reason why. Apparently we can’t support a different case for [T; 0] as it doesn’t require T to be Default.

4 Likes

When/if we get bounds on const generics, std will be able to have both impls:

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

impl<T> Default for [T; 0] {
    fn default() -> [T; 0] {
        []
    }
}

Would be cool

5 Likes

Why does it need 2 implementations? Can't const N include 0?

The non-zero cases require T: Default, but the existing 0 implementation does not require T: Default.

1 Like

Opened over 6 years ago:

Note that const bounds by themselves are not enough, you also need the compiler to support:

  • determining that the two impls are not overlapping (i.e. treating const bounds as opaque won't be enough);
  • determining that given a T: Default, const N: usize then [T; N]: Default will hold (irrespective of N!) because the two impl together exhaustively cover all the possible cases.

Both require advanced analysis on the const bounds used and I would not bet on either of them being initially supported.

9 Likes

If only we had specialization. Then this issue be avoided by letting the developer specify a priority [^1]?

#[priority(0)] impl … Default for [T; N] {…}

#[priority(10)] impl … Default for [T; 0] {…}

Then you don’t need the N >= 1 bound, don’t get into problems with checking exhaustiveness and it’s clear/easier to see which impl gets priority: The highest-priority one that can be used in this situation (wouldn’t result in a compilation error) is used.

[^1]: I’m probably missing something here and it isn’t this easy. Yes, I’m aware of the lifetime problems this can cause. I don’t know all details, but I can’t see any lifetimes that could cause an issue here.

I don't particularly like priority systems, however I believe specialization with the lattice rule should allow writing this kind specialization.

4 Likes

why? when I can do it like this

impl Default for T{
fn default()->T{ T::default }
}

This will produce conflicting impls, as all the other types that already implement Default will be in violation. This will then produce E0119. Also, this will produce endless recursion!

impl<T> Default for T{
    fn default()->T{ T::default() } // calls T::default() again and again!
}
1 Like

This seems like a design mistake. It doesn't seem particularly useful to have that impl for non-Default T and 0 length. Looks as if somebody implemented that just because they could? Or is it actually useful?

1 Like

It is probably indeed better to have a generic impl for Default types, but the zero-length impl was added before we had const generics.

2 Likes

Is it possible to change Default impl in a later edition?

In case we know N is 0, we could directly write [] instead of Default::default().

But for T: Default, creating a [T;N] requires much more code than Default::default().

[(); N].map(|_|Default::default())

It might be better to implement Default [T;N] for T: Default

2 Likes

Editions can't change the behavior of the trait solver. Editions can only do things that could be done by writing different code, but there is no different code that can be written to sometimes hide a trait implementation; whether a trait implementation exists is a global fact.

I'm not commenting on this specific change but I don't think this quote is really true? Editions can change the language itself. The only restriction is that it must still be possible to interop with crates written for a different edition. (There are also practical limitations but those can at least potentially be overcome)

It’s more fine-grained than that. Because macros exist, edition-specific behaviors generally aren’t actually triggered by the edition of the crate, but the edition of each token. So, you can actually write a crate that is made up of parts from all editions at once — all language features attributable to ā€œthis token in this editionā€ are available simultaneously.

That said, I don’t think there is a consistent set of rules which would be keyed on edition and allow this change to Default. Trait solving doesnā€˜t simply match up a specific T: Default bound written in the source code (of a particular edition) to the available impls of Default (in their own editions); you can also have intermediate steps where other traits and bounds get involved. Suppose we have a library crate with:

pub trait Something {}

impl<T> Something for Option<T> where T: Default {}

Then in another dependent crate, we try to make use of Option<[u8: 256]>: Something, or Option<[DoesNotImplDefault; 0]>: Something. Which crate's edition decides whether the const generic impl or the empty array impl is visible? What principle ensures that these do not produce inconsistent and therefore unsound results, in case Something had an associated type and there were other impls of Something that might apply?

3 Likes

Specifically, this should theoretically be a sound usage of specialization:

impl<T: Default, const N: usize> Default for [T; N] {
    default fn default() -> [T; N] {
        array::from_fn(|_| T::default())
    }
}

impl<T> Default for [T; 0] {
    default fn default() -> [T; 0] {
        []
    }
}

impl<T: Default> Default for [T; 0] {
    override fn default() -> [T; 0] {
        []
    }
}

but the difficulty comes from teaching the compiler to prove that this is sound. The impl selection for [T; 0] is potentially dependent on lifetimes (e.g. if you have impl Default for Type<'static>), so this is only sound specifically because the two impls are equivalent.

Perhaps this could be allowed with something along the lines of

impl<T: Default, const N: usize> Default for [T; N] {
    default fn default() -> [T; N] {
        array::from_fn(|_| T::default())
    }
}

#[bikeshed_specialization_choose_this_one_for_overlaps]
impl<T> Default for [T; 0] {
    final fn default() -> [T; 0] {
        []
    }
}

where we allow the second impl to partially specialize the first without the use of a separate lattice impl because we have explicitly noted which impl should be used in the overlap (and the chosen impl is "always applicable" (doesn't introduce non-const bounds)).

3 Likes

I think this is an issue about whether we care enough about impl<T> Default for [T; 0] breaking versus the possibilities available with a const generics impl. In my opinion, the latter is better, and it is a time to break from the past, as even without the former, we can still use []. I may be wrong though...

Rust does not break backward compatibility. Except for soundness and security issues, and even there only when absolutely required.

However, sometimes theoretical backwards compatibility breaks are accepted if it can be shown via a crater run that no one actually depends a specific behaviour.