-1

In Rust a reference is effectively a raw pointer with constraints on how it can be used, just as references are in C++.

type T = i64;
let my_variable: &T;

In C++, unlike a raw pointer, a reference cannot be nullptr (point to address 0) and it must be initialized when it is created. Broadly speaking, it is the same in Rust.

In Rust, a Box type is effectively a smart pointer, analogous to C++ std::unique_ptr.

On the subject of traits, for which there is no direct analogy in C++:

We can pass a trait object to a function using a explicit reference.

trait Trait;

fn func(arg: &dyn Trait);

However, the following does not compile:

trait Trait;

fn func(arg: dyn Trait);

This is the reason:

arg: doesn't have a size known at compile-time

It seem that dyn Trait is not actually a fat pointer itself, but that the "reference" version &dyn Trait is?

Box-ing the trait does compile...

fn func(arg: Box<dyn Trait>)

The question is why? I'm not totally sure this question is particularly meaningful. It seems that Box is treated by the Rust compiler in a somewhat special way, anyway.

I would have expected to see this instead:

fn func(arg: Box<&dyn Trait>)

And in fact, this seems to compile also.

So - what would the difference between these two cases be?

  • Box<dyn Trait>
  • Box<&dyn Trait>

and

  • dyn Trait - (not a meaningful concept)?
  • &dyn Trait
FreelanceConsultant
  • 13,167
  • 27
  • 115
  • 225
  • 2
    Does this answer your question? [What does "dyn" mean in a type?](https://stackoverflow.com/questions/50650070/what-does-dyn-mean-in-a-type) and links therein. – Brian61354270 Jul 29 '23 at 14:48
  • 1
    To draw a C++ analogy, the behavior of `dyn T` is essentially (exactly?) the same as abstract types in C++. If `T` is an abstract type, you can't pass a `T` to a function, by you can pass a `T*` or `T&` or `std::unique_ptr`. An abstract type is just an opaque front to some actual non-abstract type, which may have any size but which provides some consistent interface that the abstract type describes (e.g., certain vtable entries) – Brian61354270 Jul 29 '23 at 14:52
  • @Brian61354270 It's quite different from C++ though because in C++ abstract functions are implemented as part of the type, whereas in Rust behavior and data are more distinctly separated. Traits can be written without any type, whereas in C++ even an Abstract class (interface) has a type. – FreelanceConsultant Jul 29 '23 at 15:06
  • 1
    It's only different in how the languages describe them. C++'s ABCs and Rust's trait objects are just the "language level frontends" for the idea of having arbitrary runtime objects with consistent vtables. – Brian61354270 Jul 29 '23 at 15:12
  • @Brian61354270 Well, if you go all the way to machine code you can argue that all languages are the same... assuming they are Turing complete I suppose... They really are not the same at all conceptually though. You cannot implement an abstract interface for `float` in C++. You can implement a trait for `f32` in Rust. The fact they (functions and data) are conceptually segregated, which is really much more meaningful than bonding behaviors to types (C++ classes), means they really are quite different. – FreelanceConsultant Jul 30 '23 at 09:22

2 Answers2

1

The dyn Trait type is an unsized type. This means that we cannot know the size of a value of type dyn Trait at compile time. In Rust, the type of any variable cannot be an unsized type.

However, regardless of whether T: Sized (that is, regardless of whether type T has a known size), the types &T, Box<T>, *mut T, etc. are all sized. A pointer to an unsized value will contain extra information about the value itself. In particular, a pointer to dyn Trait will contain a pointer to the underlying value, plus a pointer to the vtable for the value's trait implementation. Such a pointer is called a "fat pointer".

It seem that dyn Trait is not actually a fat pointer itself, but that the "reference" version &dyn Trait is

This is exactly right, except that &dyn Trait is a type in its own right, just like &i32 is a different type than i32 is. Calling it "the 'reference' version" is thus slightly inaccurate.

A Box<&dyn Trait> is a Box which holds a &dyn Trait inside it. Thus, there are two levels of indirection going on.

By contrast, a Box<dyn Trait> holds a dyn Trait inside it. That is, a Box<dyn Trait> consists of a pointer to a heap-allocated value of some unknown type T, where T: Trait, together with a pointer to the v-table for T's implementation of Trait.

Finally, you should generally not use dyn Trait to make a function polymorphic. dyn Trait is typically used to make containers able to hold any item that that implements Trait. Instead of

fn func(arg: Box<dyn Trait>)

you should write

fn func(arg: impl Trait)

or, equivalently,

fn func<T: Trait>(arg: T)

The latter declarations will implement func using static, rather than dynamic, polymorphism.

Mark Saving
  • 1,752
  • 7
  • 11
  • On the latter point - what I was trying to build requires dynamic polymorphism, in this case related to return values, which need to be a dynamically dispatched return type, contained inside a Box. Does that explain why I went with this approach or do you still think that taking the static compiled approach is the correct solution? – FreelanceConsultant Jul 31 '23 at 09:57
0

impl Trait is more akin to a concept template constraint, and thus static (or compile-time) polymorphism.

dyn Trait could be assimilated to the way a Trait* or Trait& work in the sense that they can both hold an instance to an implementation of T instead of just Trait which in C++ could be viewed as an abstract class with only pure virtual functions. And as such, it's used mainly for dynamic (or runtime) polymorphism.

Although you need a Box<dyn Trait> (instead of just dyn Trait) because Rust only wants same size objects on non-template / non-generics functions (1 layout for 1 function, vs N layouts for N functions). That's because Box is more or less a pointer, so the size is the same for any type.

It's for the same reasons you'd use Trait* instead of Trait in C++:

  • If the type is abstract, you can't have instances of it
  • You'd be truncating the object

You also noticed you can use &dyn Trait which would be the equivalent of a Trait& in C++.

Vivick
  • 3,434
  • 2
  • 12
  • 25
  • 1
    I don't really see the concept analogy. The point of `dyn X` is that we don't know the type at compile time. – Brian61354270 Jul 29 '23 at 15:20
  • `std::integral auto x` is the same, you accept anything that satisfies `std::integral` but don't really know it's type since you're using auto. You can't have an instance of a concept like you can't have an instance of a trait. You can have an instance of some type that satisfies a concept, or of some struct that implements that trait. – Vivick Jul 29 '23 at 21:18
  • 3
    I think you're thinking along the lines of generics / `impl Trait` instead of `dyn Trait`. It's a bit muddled since Rust uses traits for both its static and dynamic dispatch features, but this question is about the dynamic dispatch use case. With `std::integral auto x`, you're constraining the static type of `x`, but it's still something that is fully known and substituted at compile time (just ask `decltype`). `dyn Trait` is something very different, since it models a type that isn't known at compile time and which may end up being anything of any size at runtime. – Brian61354270 Jul 29 '23 at 21:39
  • I see, I'll update my answer – Vivick Jul 30 '23 at 09:56
  • "That's because `Box` is more or less a pointer, so the size is the same for any type." Incorrect. A `Box` is twice as large as a normal pointer. – Mark Saving Jul 30 '23 at 23:02
  • From the Rust docs: A pointer type that uniquely owns a heap allocation of type T – Vivick Jul 31 '23 at 18:00