简体   繁体   中英

Why does the rust compiler require a type annotation for Option<&impl Trait>?

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 (ie 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"

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM