Skip to content

RFC: The ConstDefault trait #2204

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 8 commits into from
299 changes: 299 additions & 0 deletions text/0000-const-default.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,299 @@
- Feature Name: const_default
- Start Date: 2017-10-17
- RFC PR: (leave this empty)
- Rust Issue: (leave this empty)

# Summary
[summary]: #summary

1. Adds the trait `ConstDefault` to libcore defined as:

```rust
pub trait ConstDefault: Default { const DEFAULT: Self; }
```

2. Adds impls for all types which are `Default` and where the returned value in
`default` can be `const`. This includes impls for tuples where all factors are
`ConstDefault`.

3. Enables deriving of `ConstDefault` for structs iff all fields are also
`ConstDefault`. When `Default` and `ConstDefault` are derived together,
`fn default()` is derived as `= Self::DEFAULT`.

# Motivation
[motivation]: #motivation

The `Default` trait gives a lot of expressive power to the developer. However,
`Default` makes no compile-time guarantees about the cheapness of using `default`
for a particular type. It can be useful to statically ensure that any default
value of a type does not have large unforseen runtime costs.

With a default `const` value for a type, the developer can be more certain
(large stack allocated arrays may still be costly) that constructing the default
value is cheap.

An additional motivation is having a way of getting a default constant
value when dealing with `const fn` as well as const generics. For such
constexpr + generics to work well, more traits may however be required in the future.

# Guide-level explanation
[guide-level-explanation]: #guide-level-explanation

## The trait

The following trait:

```rust
pub trait ConstDefault: Default {
const DEFAULT: Self;
}
```

is added to `core::default` and re-exported in `std::default`.

This trait should be directly used if:
+ you want to be sure that the default value does not depend on the runtime.
+ you want to use the default value in a `const fn`.

You may also, at your leisure, continue using `Default` for type `T` which
will yield `<T as ConstDefault>::DEFAULT`. This is especially true if you are
a newcomer to the language as `const` may be considered an advanced topic.

## `impls` for standard library

Then, `impl`s are added to the types that exist in the standard library which
are `Default` and for which the default value can be `const`.

For the numeric types, with `usize` as an example, the `impl`s like the
following are added:

```rust
impl ConstDefault for usize {
const DEFAULT: Self = 0;
}
```

Another example is for `()`, which less useful, but nonetheless informative:
```rust
impl ConstDefault for () {
const DEFAULT: Self = ();
}
```

Another, more interesting, case is for `Option<T>`:
```rust
impl ConstDefault for Option<T> {
const DEFAULT: Self = None;
}
```

Equally interesting is the case for tuples:

```rust
impl<T0, T1> ConstDefault for (T0, T1)
where
T0: ConstDefault,
T1: ConstDefault,
{
const DEFAULT: Self = (T0::DEFAULT, T1::DEFAULT);
}

impl<T0, T1, T2> ConstDefault for (T0, T1, T2)
where
T0: ConstDefault,
T1: ConstDefault,
T2: ConstDefault,
{
const DEFAULT: Self = (T0::DEFAULT, T1::DEFAULT, T2::DEFAULT);
}

// and so on..
```

For arrays, impls like the following can be added:

```rust
impl<T: ConstDefault> ConstDefault for [T; 2] {
const DEFAULT: Self = [T::DEFAULT, T::DEFAULT];
}
```

## Deriving

Just as you can `#[derive(Default)]`, so will you be able to
`#[derive(ConstDefault)]` iff all of the type's fields implement `ConstDefault`.
When derived, the type will use `<Field as ConstDefault>::DEFAULT` where
`Field` is each field's type.

Notably also, since `ConstDefault` implies `Default`, it is a logic error on the
part of the programmer for `Self::default() != Self::DEFAULT`, wherefore
the compiler, when seeing `Default` and `ConstDefault` being derived together
will emit:

```rust
impl Default for DerivedForType {
fn default() -> Self { Self::DEFAULT }
}
```

Assuming a struct with type parameters like `struct Foo<A>(A);`,
the compiler derives:

```rust
impl<A: Default> Default for Foo<A> {
default fn default() -> Self { Foo(A::default()) }
}

impl<A: ConstDefault> Default for Foo<A> {
fn default() -> Self { Self::DEFAULT }
}

impl<A: ConstDefault> ConstDefault for Foo<A> {
const DEFAULT: Self = Foo(A::DEFAULT);
}
```

# Reference-level explanation
[reference-level-explanation]: #reference-level-explanation

## The trait

The following is added to `core::default` (libcore) as well as `std::default`
(stdlib, reexported):

```rust
pub trait ConstDefault: Default {
const DEFAULT: Self;
}
```

## `impls` for standard library

Impls are added for all types which are `Default` and where the returned
value in `<T as Default>::default()` can be `const`.

Many of these `Default` `impl`s are generated by macros. Such macros are changed
to generate `ConstDefault` `impl`s as well as a manually encoded "blanket"
`impl` instead.

An example of how such a changed macro might look like is:

```rust
macro_rules! blanket_impl {
($type: ty) => {
impl Default for $type {
fn default() -> Self { Self::DEFAULT }
}
};
}

macro_rules! impl_cd_zero {
($($type: ty),+) => {
$(
blanket_impl!($type);
impl ConstDefault for $type {
const DEFAULT: Self = 0;
}
)+
};
}

impl_cd_zero!(u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize);
```

These macros are internal to the standard library which is free to achieve
equivalent results by other means.

[const generics]: https://github.com/rust-lang/rfcs/blob/master/text/2000-const-generics.md
[const repeat expressions]: https://github.com/Centril/rfcs/blob/rfc/const-repeat-expr/text/0000-const-repeat-expr.md

Impls are also generated by macro for tuples and arrays.
If implemented [const generics] and [const repeat expressions] can be used to
implement the trait for arrays of arbitrary size, otherwise, impls can be
generated by macro for arrays up to a reasonable size.

## Deriving

The mechanism and rules used for deriving `Default` are reused for `ConstDefault`.
They are however altered to produce a `const` item in the trait instead of a
function, and instead of a trait function call, the following is used for a
factor `Field` of a product type (tuples structs, normal structs - including
unit structs): `<Field as ConstDefault>::DEFAULT`.

The compiler also handles deriving `ConstDefault` in conjunction with `Default`
specially by emitting an `impl Default` that uses `Self::DEFAULT`. The compiler
is however not obliged to do this, since `Self::default() != Self::DEFAULT` must
always be upheld by the programmer. To not uphold this is a logic error.
Special-casing might however have some compile-time performance benefits.

## In relation to "Default Fields"

[RFC 1806]: https://github.com/rust-lang/rfcs/pull/1806

The currently postponed [RFC 1806], which deals with struct default field values,
allows the user to assign default values from `const` expressions to fields when
defining a `struct` as in the following example:

```rust
struct Foo {
a: &'static str,
b: bool = true,
c: i32,
}
```

That RFC argues that an alternative to the `const` requirement is to allow the
use of `Default::default()` instead of just `const` expressions. However,
since `Default` may incur non-trival runtime costs which are un-predictable,
this is not the main recommendation of the RFC. As `<T as ConstDefault>::DEFAULT`
is const, this RFC is fully compatible with that RFC.

[RFC 1806] further mandates that when deriving `Default`, supplied field defaults
are used instead of the field type's `Default` impl. If RFC 1806 is added to this
language, for the sake of consistency the same logic should also apply to
`ConstDefault`.

# Drawbacks
[drawbacks]: #drawbacks

As always, adding this comes with the cost incurred of adding a trait and in
particular all the impls that come with it in the standard library.

# Rationale and alternatives
[alternatives]: #alternatives

This design may in fact not be optimal. A more optimal solution may be to
add a `const` modifier on the bound of a trait which "magically" causes all
`fn`s in it to be considered `const fn` if possible. This bound may look like:
`T: const Default`. If there are any such implemented trait `fn`s for a given
type which can not also be considered `const fn`, then the bound will be
considered not fulfilled for the given type under impl.

The `T: const Default` approach may be considered heavy handed. In this case
it may be considered a bludgeon while the following approach is a metaphorical
scalpel: `<T as Trait>::method: ConstFn`. With this bound, the compiler is told
that `fn method` of `Trait` for `T` may be considered a `const fn`.

While the design offered by this RFC is not optimal compared to the two latter,
it is implementable today and is not blocked by: a) adding `const fn` to traits,
b) adding a `const` modifier to bounds. Such a proposal, while useful, is only
realizable far into the future. In the case of the last alternative,
Rust must be able to encode bounds for trait `fn`s as well as adding marker trait
which constrains the `fn`s to be const. This is most likely even more futuristic.

This RFC advocates that the more optimal alteratives are sufficiently far into
the future that the best course of action is to add `ConstDefault` now and then
deprecate it when **and if** any of the more optimal alternatives are added.

[RFC 1520]: https://github.com/rust-lang/rfcs/pull/1520

The `ConstDefault` proposed by this RFC was also independently discussed and
derived in the now closed [RFC 1520] as what could be achieved with generic consts.
The trait was not the actual suggestion of the RFC but rather discussed in passing.
However, the fact that the same identical trait was developed independently gives
greater confidence in its design.

# Unresolved questions
[unresolved]: #unresolved-questions

None, as of yet.