6

Given this MCVE:

fn main() {
    println!("{}", foo(None));
}

trait Trait {}
struct Struct {}
impl Trait for Struct {}

fn foo(maybe_trait: Option<&impl Trait>) -> String {
    return "hello".to_string();
}

The rust compiler is not happy:

error[E0282]: type annotations needed
 --> src\main.rs:2:20
  |
2 |     println!("{}", foo(None));
  |                    ^^^ cannot infer type for `impl Trait`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0282`.

Using type annotations makes this compile:

fn main() {
    let nothing: Option<&Struct> = None;
    println!("{}", foo(nothing));
}

trait Trait {}
struct Struct {}
impl Trait for Struct {}

fn foo(maybe_trait: Option<&impl Trait>) -> String {
    return "hello".to_string();
}

If we use Trait instead of Struct in the type annotation, there is a bit more information given to us:

warning: trait objects without an explicit `dyn` are deprecated
 --> src\main.rs:2:26
  |
2 |     let nothing: Option<&Trait> = None;
  |                          ^^^^^ help: use `dyn`: `dyn Trait`
  |
  = note: #[warn(bare_trait_objects)] on by default

error[E0277]: the size for values of type `dyn Trait` cannot be known at compilation time
 --> src\main.rs:3:20
  |
3 |     println!("{}", foo(nothing));
  |                    ^^^ doesn't have a size known at compile-time
  |
  = help: the trait `std::marker::Sized` is not implemented for `dyn Trait`
  = note: to learn more, visit <https://doc.rust-lang.org/book/ch19-04-advanced-types.html#dynamically-sized-types-and-the-sized-trait>
note: required by `foo`
 --> src\main.rs:10:1
  |
10| fn foo(maybe_trait: Option<&impl Trait>) -> String {
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

error: aborting due to previous error

For more information about this error, try `rustc --explain E0277`.

I understand this as "You shall not use a trait here, because then I do not know how much memory I need to allocate for this parameter".

But why is that relevant when I am passing None?
Of course, passing any concrete instance of a type implementing Trait (i.e. Struct) is okay to the compiler.


Sidenote:
I have read this answer on the difference between &dyn Trait and &impl Trait. I'm unsure when to use which, but since my program does compile with &impl Trait (when using type annotations as above) it seems like the safe choice.

If instead we make the function parameter be of type Option<&dyn Trait>, my program compiles without type annotations within main():

fn main() {
    println!("{}", foo(None));
}

trait Trait {}
struct Struct {}
impl Trait for Struct {}

fn foo(maybe_trait: Option<&dyn Trait>) -> String {
    return "hello".to_string();
}

$ cargo --version
cargo 1.37.0 (9edd08916 2019-08-02)  

$ cat Cargo.toml
[package]
name = "rdbug"
version = "0.1.0"
authors = ["redacted"]
edition = "2018"
lucidbrot
  • 5,378
  • 3
  • 39
  • 68
  • 2
    It's because the compiler must *deduce* types, it can't *guess* them even if there is only one to guess from (except in certain specific scenarios not including this one). `None` could be `Option::None` or `Option::::None`. `impl trait` in argument position works like generics, so the compiler has to know what version of `foo` to instantiate, and `None` could be anything. – trent Nov 23 '19 at 21:52
  • 1
    See for example, [Is there a way to hint to the compiler to use some kind of default generic type when using Option::None?](https://stackoverflow.com/questions/58593042/is-there-a-way-to-hint-to-the-compiler-to-use-some-kind-of-default-generic-type) (all the answers) and [Optional function argument that is specified as a trait instead of a concrete type](https://stackoverflow.com/questions/27283507/optional-function-argument-that-is-specified-as-a-trait-instead-of-a-concrete-ty). Does one of those answer your question or are you still unsure? – trent Nov 23 '19 at 21:55
  • @trentcl Does that mean the compiler is not able to consider types like `Option` and realize that a generic type does not matter for the use case? – lucidbrot Nov 23 '19 at 21:56
  • 2
    it does matter, the size of the Option depend of the type that impl the trait. – Stargateur Nov 23 '19 at 22:10
  • @Stargateur So an `Option` would always take up huge amounts of memory even if it were `None::HugeThing`? Fair enough, but if it were an `Option<&HugeThing>`, that should not matter anymore. All adresses are of the same size, no matter if they point to a trait object or struct, right? – lucidbrot Nov 23 '19 at 22:15
  • 3
    I think you're missing that `foo::HugeThing` might behave differently than `foo::OtherThing` *even when passed `None`*, and the compiler has to know to use the right one. – trent Nov 23 '19 at 23:27
  • 1
    (Also, not all references are the same size because references to unsized types are fat pointers, but even if they were, the compiler still won't pick a type for you.) – trent Nov 23 '19 at 23:30
  • I just woke up with the thought "Wait! References are fat pointers sometimes". So Thanks for confirming @trentcl. But Re: "different behaviour when None", that would only matter if the function did a type check on the `Option` or called a function where the distinction matters. Am I right that some such cases could be optimized if the references were guaranteed to have the same size? – lucidbrot Nov 24 '19 at 08:49
  • Either way, I think I'd accept your answer :) Bonus appreciation if you could explain what kind of info an unsized type reference contains – lucidbrot Nov 24 '19 at 08:51

1 Answers1

5

This:

fn foo(maybe_trait: Option<&impl Trait>) -> String {

is just syntactic sugar for this:

fn foo<T: Trait>(maybe_trait: Option<&T>) -> String {

Which means that the compiler will generate many foo functions, one for every T (type implementing Trait) that you are going to use it with. So even if you call it with None, the compiler needs to know which is the T in that case, so it can pick/generate the right function.

The way Option<T> type is represented in memory depends on how the T type is represented. The compiled assembly of the function foo depends on that. For different T the resulting assembly may look differently. (For example the enum tag that defines whether it is Some or None may be at different byte offset. It may use different registers, it may decide differently whether to unroll loops, inline functions, vectorize, ...) This is the strength of static dispatch - even that you write code with lot of abstraction, you get code fully optimized for the concrete types you actually use.

With the upcoming specialization feature you can actually manually write different implementation of foo for different subsets of T, so it is really important for the compiler to know which foo are you calling. Each may do something different with None.


On the other hand this:

fn foo(maybe_trait: Option<&dyn Trait>) -> String {

Means that there is exactly one function foo that takes Option containing a fat pointer to some type implementing Trait. If you call some method on maybe_trait inside the function, the call goes thru dynamic dispatch.

Since there is exactly one function foo, you don't have to say anything about the type when using None, there is only one.

But dynamic dispatch comes at cost - this one function is not optimized for any specific T, it works with every T dynamically.

michalsrb
  • 4,703
  • 1
  • 18
  • 35
  • Thanks! Together with the comments by @trenctl this is perfect. I'll just wait a little before accepting so that he would get a chance to write an answer as well, since he explained some good points as well – lucidbrot Nov 24 '19 at 20:24
  • 1
    @lucidbrot Thanks, but unless there's something significant in my comments you think is not explained in this answer, I'll let michalsrb have the checkmark :) Glad I was able to help! – trent Nov 24 '19 at 23:27
  • This finally made it click for me, great answer. – detly Nov 08 '20 at 11:04