Default parameter mechanics
Default parameters in Rust are not as convenient as one might wish. The RFC for default type parameters was never fully completed; in particular, the "inference falls back to defaults" parts have been delayed indefinitely. As a result, there are times where default parameters don't kick in, and you have to either be explicit or use other workarounds. It can also be unclear why the workarounds act differently.
Default parameters are also not in the reference yet.
This page exists to explain the mechanics behind default parameters as they exist today, and to clear up exactly what the workarounds mean. For an exploration on how the interaction between inference and default parameters could be defined in the future, I recommend this wonderful blog post by Gankra.
Motivation
The most likely reason you'll run into default parameters not "working" is because
some expression desugars to replacing all type (and const
) parameters with inference
variables, in combination with the fact that inference variables do not fall back to the
defaults.
What are inference variables? For types, an inference variable is the same as the
"wildcard type" _
, which tells the compiler to infer the type for you.
_
cannot be used for const
parameters as of yet,
but they can still be inferred implicitly.
(For most of this guide, we'll be focused on types;
there's a subsection about const
parameters specifically later.)
Let's see some examples of compilation failures involving defaulted parameters:
#![allow(unused)] fn main() { use std::collections::HashSet; // `HashSet` will be our running example for a type with both required // (non-defaulted, non-lifetime) and defaulted parameters // struct HashSet<Key, S = RandomState> { .. } // The `insert` is enough for the compiler to infer the `Key` parameter, but // not the `S` parameter let mut hs = HashSet::default(); hs.insert(String::new()); // This means the same thing: *all* type (and const) parameters became // inference variables let mut hs = HashSet::<_, _>::default(); hs.insert(String::new()); }
This can be confusing because similar code just works:
#![allow(unused)] fn main() { use std::collections::HashSet; // This compiles, but the compiler can figure `Key` out on its own, so why? let mut hs = HashSet::<String>::default(); hs.insert(String::new()); // And in fact... this compiles too! let mut hs = HashSet::<_>::default(); hs.insert(String::new()); // `new` doesn't have this problem, which may also be confusing let mut hs = HashSet::new(); hs.insert(String::new()); }
The errors can also arise when the type has defaults for of all the type (and const
) parameters:
#![allow(unused)] fn main() { // This will be our running example for a type where all non-lifetime // parameters have defaults pub enum Foo<T = String> { Bar(T), Baz, } // This fails because the elided parameter desugars to an inference variable let foo = Foo::Baz; // So this means the exact same thing let foo = Foo::<_>::Baz; }
And some of the workarounds may be even more confusing:
#![allow(unused)] fn main() { pub enum Foo<T = String> { Bar(T), Baz } // This works! let foo = <Foo>::Baz; }
We want to explain exactly which expressions end up being problematic, and why the workarounds solve the problem.
The explanations in brief
First let's tackle why just wrapping the type in <>
worked for that last example.
#![allow(unused)] fn main() { pub enum Foo<T = String> { Bar(T), Baz } let foo = <Foo>::Baz; }
The leading <Foo>::
notation is called a
"qualified path type".
And the short answer to why it works is that, with respect to elided default
parameters, types in <>
s act the same as type ascription:
#![allow(unused)] fn main() { pub enum Foo<T = String> { Bar(T), Baz } // Also works let foo: Foo = Foo::Baz; }
Type ascription uses default parameters in a way that's probably closer to your intuition.
(We explore the details below.)
Note that types act like type ascription in <>
elsewhere too, such as
within a turbofish, not just as a qualified path type.
As for the difference here:
#![allow(unused)] fn main() { use std::collections::HashSet; // This fails if we change `HashSet::new()` to `HashSet::default()` let mut hs = HashSet::new(); hs.insert(String::new()); }
The example only works because HashSet::new
(and a number of other methods) is only defined for HashSet<_, RandomState>
.
In contrast, Default
is implemented for all possible HashSet<_, _>
. So
in a sense, this is a workaround on the side of the HashSet
implementation!
If inference and default parameters worked together,
new
would presumably be defined for all possible hashers, too.
Finally, let's look at this workaround:
#![allow(unused)] fn main() { use std::collections::HashSet; // Remember, `HashSet::default()` fails let mut hs = HashSet::<_>::default(); hs.insert(String::new()); }
The key difference here is that if no required parameters are specified,
then all the type (and const
) parameters -- including defaulted parameters
-- are filled in with inference variables. But if one or more non-lifetime
parameter is specified, it desugars to a qualified type path -- where default
parameters act the same as they do in type ascription.
#![allow(unused)] fn main() { use std::collections::HashSet; // These are all the same and fail // let mut hs = HashSet::default(); // let mut hs = HashSet::<>::default(); // let mut hs = HashSet::<_, _>::default(); let mut hs = <HashSet::<_, _>>::default(); hs.insert(String::new()); }
#![allow(unused)] fn main() { use std::collections::HashSet; // These are the same and succeed. // let mut hs = HashSet::<_>::default(); let mut hs = <HashSet<_>>::default(); hs.insert(String::new()); }
As is clear from the example, using _
explicitly counts as specifying a type
parameter. Also note that the desugaring to "all parameters are inference
variables" only happens when the type is not inside <>
s.
Type position mechanics in more detail
By "type position", we mean contexts where the language expects a type specifically. This includes variable type ascription, implementation headers, type parameter fields themselves, and qualified path types.
In type position, you can only elide default parameters. Elided default parameters are
replaced by their default types (or const
values) specifically (i.e. not inference variables).
Let's see some examples:
#![allow(unused)] fn main() { use std::collections::HashSet; use std::hash::RandomState; enum Foo<T = String> { Bar(T), Baz, } // These ascriptions mean the same thing // vvvvvvvvvvv let e: Foo = Foo::Baz; let e: Foo<> = Foo::Baz; let e: Foo<String> = Foo::Baz; // These ascriptions mean the same thing // vvvvvvvvvvvvvvvvvvvvvvvvvvvv let hs: HashSet<String> = Default::default(); let hs: HashSet<String, RandomState> = Default::default(); }
The following errors demonstrate that elided parameters aren't inference variables, and that inference variables don't fall back to the defaults.
#![allow(unused)] fn main() { enum Foo<T = String> { Bar(T), Baz, } // Fails due to ambiguity let e: Foo<_> = Foo::Baz; }
#![allow(unused)] fn main() { use std::collections::HashSet; use std::hash::RandomState; // Fails due to ambiguity let hs: HashSet<String, _> = Default::default(); }
#![allow(unused)] fn main() { enum Foo<T = String> { Bar(T), Baz, } // Fails because the elided type is exactly the default type (`String`) let e: Foo = Foo::Bar(0); }
The final example is the opposite situation from most of the examples we've seen:
it's a case where you want inference to override defaults. If you made the ascription
Foo<_>
it will compile (but a more trivial fix for this particular example is to
just remove the redundant ascription).
More about qualified path expressions
Types inside of <>
s are in type position, and that includes qualified path expressions.
A qualified path expressions
is when you have some path expression that starts with a segment contained in
<>
. They were defined in RFC 0132
and they come in two different forms:
#![allow(unused)] fn main() { // vvvvvvvv `<T>` where `T` is a type let s = <String>::default(); // vvvvvvvvvvvvvvvvvvv `<T as Tr>` where `T` is a type and `Tr` is a trait let s = <String as Default>::default(); }
The first form can resolve to inherent functions or trait methods, whereas
the second form can only resolve to the named trait's methods. Rust doesn't
have "trait inference variables", so the trait must be named; you can't use
_
in place of the trait, for example. (You can still use it in place of
the trait's type parameters.)
Traits
Default parameters for traits work the same as default parameters for types, both inside and outside of "type position". When thinking of traits in paths as sugar for qualified paths, the desugaring is like so:
#![allow(unused)] fn main() { trait Trait<One, Two = String>: Sized { fn foo(self) -> (Self, One, Two) where One: Default, Two: Default { (self, One::default(), Two::default()) } } impl<T, U> Trait<T, U> for i32 {} impl<T, U> Trait<T, U> for f64 {} // Failing versions //let _: (i32, (), _) = Trait::foo(0); //let _: (i32, (), _) = Trait::<_, _>::foo(0); let _: (i32, (), _) = <_ as Trait<_, _>>::foo(0); // ^^^^^^^^^^^^^^^^^^ }
#![allow(unused)] fn main() { trait Trait<One, Two = String>: Sized { fn foo(self) -> (Self, One, Two) where One: Default, Two: Default { (self, One::default(), Two::default()) } } impl<T, U> Trait<T, U> for i32 {} impl<T, U> Trait<T, U> for f64 {} // Working versions let _: (i32, (), _) = Trait::<_>::foo(0); let _: (i32, (), _) = <_ as Trait<_>>::foo(0); // ^^^^^^^^^^^^^^^ }
The only new thing of note is that the implementing type is an inference variable in this case.
Mostly historical side note
Before edition 2021, it's possible to leave the dyn
off of dyn Trait
types
(although it does fire a lint). This means that the same name can refer to either
a trait, or a type (the trait object type). Which one is used depends on the
context.
For example:
let _: i32 = Trait::name(0.0);
// If `Trait` has a method called `name`, that is is
let _: i32 = <_ as Trait>::name(0.0);
// But if it does not, and `dyn Trait` has a method called `name`, this is
let _: i32 = <dyn Trait>::name(0.0)
// And the following line is always referring to `dyn Trait`
let _: i32 = <Trait>::name(0.0);
More about types in expressions
In this section, "types in expressions" refers to types which are in expressions
but not within <>
(e.g. not a qualified path type or a type parameter). These
are the positions where it is required to use turbofish (e.g. Vec::<String>
)
instead of just appending the parameter list (e.g. Vec<String>
).
In these positions, it is always allowed to elide all the type and const
parameters, even if there are required (i.e. non-defaulted, non-lifetime)
parameters. When you do so -- even if all the type and const
parameters
have defaults -- the behavior is the same as using type inference variables
(_
) for all the parameters.
If you do not elide all non-lifetime parameters -- that is, if you specify one or
more type parameter or const
parameter -- then you must specify all required
parameters. Or in other words: if you specify at least one type or const
parameter, you can only elide defaulted parameters (and lifetimes).
The behavior of elided defaulted parameters is as follows:
- If you specify zero non-lifetime parameters
- Inference variables are used for all type and
const
parameters
- Inference variables are used for all type and
- If you specify one or more non-lifetime parameters
- Defaults are used for elided type and
const
parameters
- Defaults are used for elided type and
Above, we phrased the different default parameter behavior for types in expressions in
terms of desugaring to qualified type paths.
However, the behavior applies in other contexts too, such as struct
expression syntax:
#![allow(unused)] fn main() { struct Two<T, U = String> { t: T, u: U } // This is ambiguous let _ = Two { t: (), u: Default::default() }; }
#![allow(unused)] fn main() { struct Two<T, U = String> { t: T, u: U } // But this works let _ = Two::<_> { t: (), u: Default::default() }; }
Qualified path types are not allowed in this postion, so not all of the workarounds we discussed for paths are applicable.
#![allow(unused)] fn main() { struct One<T = String> { t: T } // Ambiguous let _ = One { t: Default::default() }; }
#![allow(unused)] fn main() { struct One<T = String> { t: T } // Not accepted grammatically let _ = <One> { t: Default::default() }; }
Finally, there is no way to syntactically represent inferred but
non-defaulted const
parameters in qualified path types (or any
other type-annotation-like position).
#![allow(unused)] fn main() { struct Pixel<const N: usize>([u8; N]); impl<const N: usize> Default for Pixel<N> { fn default() -> Self { Self([0; N]) } } // Works let pixel = Pixel::default(); // These fail because `_` cannot be used for const parameters yet // let pixel = <Pixel<_>>::default(); // let pixel: Pixel<_> = Default::default(); drive_inference(pixel); fn drive_inference(_: Pixel<3>) {} }
Non-type generic parameters
This guide has mostly concentrated on type parameters. We've tried to be careful with our wording throughout the guide, but let's take a moment to talk specifically at how non-type parameters work with regards to defaults.
Lifetime parameters
Lifetime paramters can not be given defaults, and do not change any of the default parameter behavior we've discussed.
I've tried to take care to use phrases like "specify one or mmore non-lifetime parameter" instead of phrase like "empty parameter list". But just to make things more explicit: the inclusion or elision of lifetime parameters doesn't change how parameter defaults work.
For example, the below are still cases of specifying no required parameters, and thus uses an inference variable (which then fails as ambiguous).
#![allow(unused)] fn main() { pub enum Foo2<'a, T = String> { Bar(&'a T), Baz, } let foo = Foo2::<'_>::Bar; let foo = Foo2::<'static>::Bar; }
const
parameters
const
parameters defaults were stabilized in 1.59,
along with the ability to intermix const
and type parameters
in the parameter list (as otherwise the presence of a defaulted
type parameter would force all const
parameters to also have
defaults, for example).
Generally speaking, defaulted const
parameters act just like defaulted
type parameters. However, one important difference is that
_
cannot be used for const
parameters as of yet.
This does mean that some of the workarounds we've seen cannot be applied:
#![allow(unused)] fn main() { // We'll use this analogously to our `HashSet` examples struct MyArray<const N: usize, T = String>([T; N]); impl<T: Default, const N: usize> Default for MyArray<N, T> { fn default() -> Self { Self(std::array::from_fn(|_| T::default())) } } // Ambiguous for the usual reasons // let arr = MyArray::default(); // Here's what we did when `HashSet` had this problem. // But it fails because we can't use `_` for the `const` parameter! let arr = MyArray::<_>::default(); // Explicitness it is then // let arr = MyArray::<16>::default(); // (These parts are just here to make everything above work like // our `HashSet` examples worked.) drive_inference_of_length(&arr); fn drive_inference_of_length<T>(_: &MyArray<16, T>) {} }
A warning about implementations and function arguments
Default type parameters work the same in implementation headers and function argument lists as they do in other "type positions". This may be surprising with compared to elided lifetime parameters.
In implementations and function argument lists, eliding a lifetime parameter introduces a new, independent generic lifetime parameter. But eliding a type parameter never means "introduce a new generic". Elided type parameters always resolve to a single type (or error), whether that type comes from inference or a default type.
#![allow(unused)] fn main() { pub enum Foo<T = String> { Bar(T), Baz } // This is an implementation for `Foo<String>` only impl Foo { fn papers_please(&self) {} } // This is an implemenetation for all (`Sized`) `T` impl<T> Foo<T> { fn welcome(&self) {} } let foo = Foo::Bar(0); // Works foo.welcome(); // Fails foo.papers_please(); }
(Eliding lifetimes in other positions sometimes means 'static
and
sometimes means "infer this for me", but that's a topic for another
day. Lifetime parameters cannot have defaults.)
Default type parameters elsewhere
Declaring default parameters that are not on types, traits, or trait aliases either results in an error, or fires a deny-by-default lint stating that support will be removed.
Despite the lint, default parameters on functions work the same as default parameters on type declarations. However, every function has a unique type (a "function item type") which cannot be named. Because the function item type cannot be named, most of the workarounds we've talked about cannot be applied.
That being said, the case where you use a turbofish with one or more non-elided type parameter still works:
#![allow(unused)] fn main() { #[allow(invalid_type_param_default)] fn example<X, Y: Default = String>() -> Y { Y::default() } let s = example::<()>(); println!("{}", std::any::type_name_of_val(&s)); }
Default parameters on impl
headers do not serve any purpose as far as I'm
aware. Implementations don't have names at all (which is why the parameters
are on the impl
keyword).
#![allow(unused)] fn main() { struct MyStruct; trait WhyThough<T, U> { } #[allow(invalid_type_param_default)] impl<T = String> WhyThough<i32, T> for MyStruct {} }
Default parameters on GATs are currently just denied, even if the lint is allowed.
#![allow(unused)] #![allow(invalid_type_param_default)] fn main() { trait MyTrait { type Gat<T = String>; } }