Difference between returning dyn Box<Trait> and impl Trait

Those two pages to which I provided links and appropriate fragments of text below seem to contradict each other. Are they differ in the purpose they are serving? What would be the typical usage of one and the other?

Thank you.

If your function returns a type that implements MyTrait , you can write its return type as -> impl MyTrait

Unlike other languages, if you have a trait like Animal , you can't write a function that returns Animal , because its different implementations will need different amounts of memory.

5 Likes

In the first case, function is returning not a trait, but some concrete type which implements MyTrait; it just doesn't tell the caller exactly what type it is, but it is fixed, and compiler knows that.

2 Likes

They don't. dyn Trait and impl Trait are different implementations of the same high-level idea of an "existential type". This means that in both cases, you get a single type, but how they are laid out in memory is different.

dyn Trait is unsized (or, more accurately, dynamically-sized), which means that its size is not known at compile time. It's similar to a slice [T] in this regard. This means that you can only ever handle a dyn Trait behind some sort of pointer or indirection, like a reference &dyn Trait or an owning Box<dyn Trait>. It also employs dynamic dispatch, which means that the methods you call on a dyn Trait will go through a vtable and function pointers.

This indirect nature of dyn Trait means that it is a proper type in itself, and that the same dyn Trait can be created from multiple, distinct concrete types. For example, you can do this:

use std::fmt::Display;

fn foo(is_number: bool) -> Box<dyn Display> {
    if is_number {
        Box::new(42_u32)
    } else {
        Box::new(String::from("this is a string"))
    }
}

In contrast, impl Trait is a completely static, compile-time-only construct. The single, concrete type behind impl Trait is known to the compiler; it just hides it from the programmer intentionally. This means that an impl Trait created from a Sized value is also Sized and can be returned or passed by value, without any sort of indirection. It also uses static dispatch, so the functions you call on an impl Trait value are also known to the compiler at compile time.

However, this has one important consequence: a given impl Trait cannot be created from several, distinct types. If you replace Box<dyn Trait> with impl Trait in the code above, it won't compile anymore. Or, to put it differently, impl Trait is not a proper type in itself, it's a placeholder for a concrete type which is known to the compiler, but not to the programmer. Therefore, you must treat it as a single type in a given context.

Well, it should be obvious from the above: they have different trade-offs.

  • Sometimes, you want dynamic dispatch, because you need function pointers and vtables, and maybe you want to store heterogeneous values in a collection. The prime example is HTTP handler functions in a webserver, corresponding to different routes/paths. The routing logic of the web server usually needs to store the various request handler functions in a central data structure, so it will probably create something like a HashMap<String, Box<dyn FnMut(Request) -> Response>> internally.
  • When you are building a data structure out of types that you don't necessarily know upfront, you might also want to use Box<dyn Trait> instead of making every type of the data structure generic. This can make its manipulation easier on the programmer's eyes.
  • Trait objects can also be used for shaving off a couple of percents from the compilation time, since they require less type wrangling in the internal representation of the compiler as they avoid monomorphization.
  • In contrast, impl Trait is useful when you have a single, concrete type that you want to hide for some reason, or if you are simply too lazy to type it out (e.g. when it's a very complicated, nested Iterator adaptor).
  • impl Trait can also be used for returning closures, of which the type simply cannot be named. impl Trait avoids heap allocation and is Sized, so it can be more efficient and more convenient to work with in some cases.
30 Likes

Thanks guys, that helped.

Just to round-out a related question, I seem to recall that a type that implements a trait object, takes-on the trait object’s type; i.e., it essentially forgets its original type. How does that relate to dyn Trait?

dyn Trait is a trait object. It implements the trait Trait, but has no other access to items from the object it was created from. Because it's a normal type, you can implement additional methods for it with an impl block (subject to coherence rules):

trait A {
    fn get_int(&self)->i32;
}

impl dyn A {
    // Self here is `dyn A`
    fn get_double(&self)->i32 { 2*self.get_int() }
}

In this case, get_double can only be called on the trait object dyn A and not on any other type that implements A.

3 Likes

Got it. So the relation is dyn Trait is a trait object. Apologies for what comes next, so they are “one in the same”; anywhere “dyn Trait” is used I could say “trait object and vice versa. Yes?

Also, in the docs

A trait object is an opaque value of another type that implements a set of traits.
... Due to the opaqueness of which concrete type the value is of, trait objects are dynamically sized types.

Could be “read-in” that trait objects are not “concrete types”. However, given the following,

... dyn Trait is a type constructor, so indeed not a concrete type (per the inference in the docs), but once instantiated is “a proper type in itself” (per the quote).
=> The feature of the trait: it does not reveal the type of the type parameter used to instantiate it.

Yes?

I’m not sure that’s a correct reading of it. It’s not saying it’s not concrete, it’s saying it’s not sized. This is why it must be behind a pointer/reference to be used. Much like the str type.

Yes, I think so.

Not really. dyn Trait is a concrete type. It is created from one (or more) other concrete types using a type conversion, usually an unsized coercion. Just like you can create a reference-to-slice from a reference-to-array using coercion. This doesn't mean that either the slice or the array is not a concrete type: both are, they are just distinct types. Actually, I can stretch this analogy even further. Just like a slice can be indexed in the same way an array can, a trait object can be the receiver of methods, just like the underlying type it was coerced from, and they behave identically. However, one of these has a more "static" nature (sized arrays and the underlying type of a trait object) in both pairs, while the other is more "dynamic" (unsized slices and trait objects themselves).

No, it's a type. Type constructors are generic "types" (or more appropriately, type-level functions) like Vec, that only become proper types once you apply them to some generic type parameters. So, Vec is a type constructor. dyn Trait has no generic arguments – it's a type in itself.

4 Likes

Correct.

dyn Trait is categorically not a type constructor: There is no way to produce another type from dyn Trait. If anything, it's a type that represents a restricted view of some other type. Because the compiler doesn't know which type it's a view of, it doesn't know how much space would need to be reserved on the stack for it, and so requires dyn Trait instances to only be used behind some indirection.

4 Likes

I agree. I provided a bit more of the quote to show the flow. I still agree :)). If what I’m saying is otherwise correct, I can see how what is being described in this post might be missed without a careful read (guilty as charged :)).

Perfect. I’m so glad I asked the question.

And of course, it's now obvious; there is no type parameter in the example used:

trait Printable {
    fn stringify(&self) -> String;
}

Ok, so my point missed the point.

Another go

With regular traits I'm "bolting on" additional features to an existing type. The existing type is now part of a set that can be specified using existential quantification, i.e., the where clause that limits T to a subset of infinity. All the while, retaining its type*.

In contrast, the T used to implement dyn Trait, becomes the type Trait (e.g., my T is now Printable). A cast to Trait where Rust loses sight of the original type (no original type retention).

So, does that mean that using a dyn Trait to existentially quantify a T is an oxymoron? i.e., it makes no sense to include a dyn Trait in a where clause?

* perhaps the source of confusion

* In likely a diabolical way (yes, evil-like :)), I can see how I temporarily considered dyn Trait as a type constructor (which it isn't). The concept of dyn Traits is the idea of treating any number of types T as a single type.

Here it is: Traits in general have a "generic-like" quality to them from a construction perspective. Vec is kind: * -> *, clearly a type constructor. Traits such as Display are kind: *, so not a type constructor. However, with eyes blurred, traits such as Display are only concrete in their ability to specify a subset of T. In other words, they require T to be... useful (not concrete): impl Display for <Type that provides substrate for Display to exist>. The whole purpose of traits is to provide a way to treat many types as a single type. This is in contrast to Vec<T> where the end type e.g., Vec<i32> is different than Vec<&str>.

However, if my final question about it not being useful to include dyn Trait in a where clause is correct, then dyn Trait has more common with the likes of Vec<T> than it is to Display because of where it fits in a type signature (and all that that means). If it's more like Vec<T>, it's not because of a type constructor-like capacity, but because of it being a single concrete type constructed from any T... the confusion for me and value in clearing this up.

See also this other old thread: What's the difference between T: Trait and dyn Trait? - #3 by Yandros

It’s rare, but could make sense when there are other traits implemented directly on dyn Trait itself. dyn Trait is a type, so it will always be on the left side of a bound:

This is legal and possibly useful in niche cases:

impl<T> ... where dyn TraitA<T>: TraitB {}

This is not allowed, because dyn Trait is a type, not a trait:

impl<T> ... where T: dyn Trait {}

This terminology is a bit imprecise. Nothing implements dyn Trait because dyn Trait is a type, and not a trait. Trait is always a trait, and never a type[1]. Any object of type T: Trait + Sized + ‘static can be coerced into an object of type dyn Trait. These two objects refer to the same block of physical memory, but their types are otherwise unrelated.

[1] In modern Rust with no warnings.

2 Likes

Got it (I think :)). So the cast to a dyn trait is something the compiler does when we are placing the representation in a context that requires a trait object. Thus, using the dyn keyword is not required by the compiler to disambiguate, but rather to convey intent visually in the code?

1 Like

That's essentially correct. Old versions of Rust didn't require dyn: Any use of a trait name where a type was expected implicitly referred to a trait object. This proved overly confusing, as only some traits could be used that way (and possibly other reasons).

1 Like

Thank you. This stream was helpful.

I think this is more a type theoretic related question to existentials at all and not that specific to dyn Trait.

You can think of existentials as where constraints of the form T where T:Trait, so obviously an existential seems to be a type parameter or constraint, but it's actually a type because "where" is an operator unifying all T's conforming to the trait "Trait" to one type.
So you can think of a Trait as a union type which is itself a concrete entity.

Perhaps is the ying to the “any and all T -> a subset of T” yang.

I’m going to use both as a way to think about it.

I state the latter as such because there is no way to implement anything “for all T” other than the identity function. That is a useful reference for me. From the place of “can’t do anything” what I can do is specified by the where clause: all T’s conforming to the Trait.

Well you can't add requirements for all types except specific builtin requirements provided by the compiler/language.
But you could theoretically provide any amount of default methods though it may increase the ambiguity surface.

Yes, constrained existentails are a form of bounded unification of types, only those are part of the union/trait which provide the required implementations.