What does assignment in a generic mean?

I saw something like this:

struct Foo<A = u32> {
    bar: A
}

I don't really get what this means... doesn't seem to be really restricting A in any way, i.e. this compiles just fine:

fn main() {
    let f = Foo { bar: 42 };
    let f = Foo { bar: 4.2 };
}

It's a default type used in case the type isn't specified and can't be guessed from the context.

1 Like

A word of warning against an unintuitive part of default type parameters

When mentioning the name Foo, it can either refer to a type, with the type parameter implicitly elided) or to the beginning of a path (such as Foo::some_thing) when used in expression context, where the type parameter is (implicitly) elided as well. For instance, consider:

// Given:
#[derive(Default)]
struct DefaultTy;

#[derive(Default)]
struct Inferred;

#[derive(Default)]
struct Foo<T = DefaultTy> {
    value: T,
}

// Consider:
let a: Foo = Default::default();
//        ^
//        no type param => use default ty
let b = Foo::default();
//          ^
//          no ty param: same as `Foo::<_>::default()` => inferred
let c = <Foo as Default>::default();
//          ^
//          no type param => use default ty
let _: DefaultTy = a.value; // OK
let _: Inferred = b.value; // OK
let _: DefaultTy = c.value; // OK

let d: Foo<_> = Default::default();
let _: Inferred = d.value; // OK

let _ = Foo::default(); // FAILS!!

While the lines for a, b, and c may look awfully similar, it turns out that:

  • a and c are mentioning Foo as a type, and thus implicitly eliding the type parameter (vs. explcitly eliding it, with Foo<_>, or explicitly feeding a type, such as with Foo<()>) is allowed, since it means "pick the default type parameter u32" (from the = u32 part in Foo's definition);

  • whereas b is mentioning Foo within an expression path, where there is no difference between implicitly elided (Foo) and explicitly elided (Foo::<_>). Both cases have the semantics of explicitly elided, which means that type inference is expected to provide the value of that type, and not the default type (as you can see in the example, it chose T = u8 by inference).

    • That is, the case b is kind of like the case d, in semantics.

All in all: there is no mechanism for a fallback to type inference: either type inference kicks in, potentially unsuccessfully (e.g., let _ = Foo::default();), or there was no type inference to begin with, since we just wanted the default type parameter shortcut.


This is why default type parameters are way less useful than we can initially imagine; their main role being to add genericity to already existing types in a retro-compatible fashion.

  • For instance, the pervasive Box<T> is actually, nowadays, the type Box<T, Allocator = Global>. That is, it has that extra Allocator parameter, which was given a default "value" of Global to make Box<T> still work (as an alias for Box<T, Global>, thus). But in order to feature Box<T, Allocator> methods that can work with arbitrary Allocators, it has currently required a whole set of distinct associated functions. So you have, for instance, Box::new_in(…), instead of Box::new(…), and so on and so forth.
4 Likes

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.