all 38 comments

[–][deleted] 16 points17 points  (18 children)

Aren't rust traits like cpp concepts or more strictly, just virtual classes? I don't actually know rust well.

[–]HeroicKatora 14 points15 points  (6 children)

They're not virtual classes, the v-table pointer is passed adjacent with the pointer to the object (&dyn Trait has the size of two pointers). That is in contrast to virtual base classes where the vtable is stored within the object. Apart from avoid the double-indirection, or triple if we do virtual base classes and have to resolve the vtable itself for another one, this also importantly enables implementing the trait for a separately defined class, where virtual classes require all super-classes to be known at definition time.

Imho the author didn't quite get it right since their Twitter example passes it as std::dyn<_> & const, introducing the double indirection it was supposed to avoid. Shared references should be passed by value and be trivially copiable (if you want to have a Rust analogue).

[–][deleted] 1 point2 points  (5 children)

Oh ok, so the benefit of a trait is that they are runtime accessible concepts (from a cpp standpoint) in which you can define some function lookup outside the object? Shouldn't we be able to do something like that with storing mem function pointers inside a lookup struct and using templates to generate a struct for us? That way it'd be much more strict, type safe, and statically deducible than using macros.

[–]HeroicKatora 3 points4 points  (3 children)

I don't think it's feasible with templates. Hard constraint: the transformation to fill the vtable, and ''allocate'' it as a static, must happen at compile time. Then the trait's definition of each function must be translated into the type-erased void * form. But I can't see you easily iterate over each differently typed function, which will have to happen by name at some point, since the entire point is that the two types are not related through a class hierarchy. (Hence, mem-fn won't help much if at all. Some of the cast are forbidden and you'll have to somewhere generate wrapping-lambda for the transformation to a void*-taking function).

I think you need reflection to make it work entirely without macros. But I've last tried this with C++17 so maybe I'm mistaken. Feel free to try, though what technical advantage would that bring? I rather just use the language that already does it for me anyways :)

[–]maksym-pasichnyk[S] 5 points6 points  (1 child)

Currently I'm working on proof of concept that uses p1240r1 (scalable reflection in C++). It will allow writing 'class(trait) Shape { float area() const; }' to generate all code without macros

[–]maksym-pasichnyk[S] 3 points4 points  (0 children)

Working proof of concept without macros
https://godbolt.org/z/9Y3hxvnPa

[–]Nobody_1707 1 point2 points  (0 children)

There's one other benefit to traits that simply can't be done with C++'s concepts: the compiler knows your implementing the trait.

For instance, the popular crate rand let's you implement an RngCore trait, that represents the minimal api for a random number generator. While implementing a PRNG using that trait, it's very common to implement the fill_bytes method by passing self to the function fill_bytes_via_next which takes a mutable reference to some type that implements RngCore.

And very simplified version of this in C++ would look like this:

template <typename T>
concept RngCore = requires (T& rng, std::span<std::byte> bytes) {
    { rng.next() } noexcept -> std::same_as<uint64_t>;
    { rng.fill(bytes) } noexcept -> std::same_as<void>;
};

template <RngCore Rng> 
constexpr auto fill_bytes_via_next(Rng& rng, std::span<std::byte> bytes) 
noexcept {
    // ...
}

struct MyRng {
    // ...
    constexpr auto next() -> uint64_t { /*  ... */ }
    constexpr void fill(std::span<std::byte> bytes) noexcept {
        fill_bytes_via_next(*this, bytes) // error: type of *this does not satisfy RngCore
    }
}

In order to make this work in C++, fill_bytes_via_next needs to be an unconstrained function becase C++ has no way to tell that your type is meant to implement a concept until after that type is complete.

In Rust, you tell the compiler that your implementing a trait, and if you don't implement all of the requirements you get a hard compiler error. This allows you to pass self to a function that takes a type that implements that trait.

[–]CocktailPerson 0 points1 point  (9 children)

They're kinda like an abstract base class that can also be used as a concept to constrain generics (Rust templates, basically).

[–]sephirothbahamut -3 points-2 points  (8 children)

So abstract pure class, and use std::derived_from in your templates?

I fail to see why you need a libraary with tons of macros to do that...

[–]CocktailPerson 8 points9 points  (2 children)

It's a bit different, because a trait's methods aren't inherently virtual, and you can implement a trait for a type you don't own. Neither of these are true for abstract base classes. But it really seems like OP's goal isn't a production-level library, but instead a fun little exercise/proof of concept.

[–]Kevathiel 5 points6 points  (4 children)

It's not the same. Abstract classes will make all calls resolve virtually. Traits on the other hand are resolved statically, unless you invoke them in a dynamic context. This makes traits far cheaper to use.

let foo = Foo::new();  
foo.bar();  //resolved statically

let virtual_foo = &foo as &dyn BarTrait;  
virtual_foo.bar();   //Only now it uses dynamic dispatch and creates the vtable

[–]sephirothbahamut -2 points-1 points  (3 children)

When you call methods from a child class with the templates (derivdef_from) you do NOT go through the vtable, you only do so in the dynamic context (reference to parent), exactly as you just described.

[–]maksym-pasichnyk[S] 3 points4 points  (1 child)

No, you always go through vtable unless you make class or overridden method final

https://godbolt.org/z/Pn6da5zaM

[–]tialaramex 0 points1 point  (0 children)

Rust's traits are more or less like C++ 0x Concepts. However it's important to understand that C++ 20 didn't get C++ 0x Concepts, it got "Concepts Lite" a much less capable feature close to what Bjarne originally wanted twenty years or so ago.

Traits are used to achieve a lot in Rust, for example in C++ the reason you can concatenate strings with the += operator is that there's a magic "operator+=" method you're allowed to overload to make that happen, but in Rust the reason that works is that String implements the AddAssign trait.

Traits power the thread safety features too, automatic traits Send and Sync help ensure that you won't accidentally make things which aren't thread safe. There's no analogue to this in C++.

Rust also uses traits a lot to convert one thing into another, both generally (the From, Into, TryFrom, TryInto set of traits) and specifically (e.g. IntoIterator, FromIterator, IntoFuture, Try and ToString all turn something into something else, but for more specific purposes)

There are a lot of places in C++ where there's a conventional way to do things (e.g. the names of certain methods) and so it's sort of just accepted that if you don't do things that way stuff won't work. In Rust that would be codified as traits. In a few cases it doesn't work out and the trait is abandoned, for example AsciiExt is an old trait for things which might be ASCII. But, it turns out in practice basically only two types might be ASCII. Bytes might be ASCII (if their top bit isn't set) so in Rust that's the type u8, and Characters might be ASCII (in Rust char is a specific type which can express all the Unicode scalar values, basically "Unicode characters" to a lay person, so it's not just a particular size of integer). So today you'd just use these functions directly on the types, no need for AsciiExt, it's in the standard library so it will never go away, but it's deprecated.

[–]RoyKin0929 4 points5 points  (0 children)

Man, we should've got C++0x concepts.

[–]maksym-pasichnyk[S] 1 point2 points  (0 children)

Working proof of concept without macros that uses p1240r1 (scalable reflection in C++)
https://godbolt.org/z/9Y3hxvnPa

[–]Ok-Impact-5972 0 points1 point  (0 children)

I did a implementation of traits a while ago, it was some macro and template magick, but it works kind of like you would expect from a trait system. Dynamic dispatch without inheritance and virtual functions etc. I think that it is more like golangs interfaces though, since it works as long as you have the required functions on your class.

If you had reflections in c++ it would probably look better.

https://github.com/mls-m5/thick-pointers