I got in touch with rust
quite early and watched its ecosystem gradually improve.
When I first used trait
, I wondered how to implement such a great feature in c++
.
Though it wasn’t an urgent need, I put it aside. Recently, I tried to implement it in a simple way.
Rust Trait
Let’s first look at an example of rust trait.
Simply put, it’s about defining interfaces, implementing interfaces, and interface polymorphism.
Actually, c++
already has a complete inheritance paradigm to implement these requirements - virtual interface, inheritance implementation, virtual polymorphism.
However, virtual functions require forced dynamic dispatch and add virtual table pointers to class instances.
So let’s try to implement trait
without using virtual inheritance.
keywords
- trait → Defines a trait.
trait Speak { fn speak(&self) -> String; }
- impl → Implements a trait for a type.
struct Dog; impl Speak for Dog { fn speak(&self) -> String { "Woof!".to_string() } }
- where → Adds trait bounds in a structured way.
fn make_speak<T>(animal: T) where T: Speak { println!("{}", animal.speak()); }
- dyn → Used for dynamic dispatch.
fn speak_dyn(animal: &dyn Speak) { println!("{}", animal.speak()); }
C++ Implementation
Complete code
The specific implementation may change, refer to the documentation in the repo
Interface Definition
When there’s no reflection for code generation, the interface needs to include delegate functionality
requires
can only be used as constraints; it cannot be materialized into concrete functions, so we need to define concrete interface classes.
Now we can define any interface class and write a set of function declarations.
Then how do we call the implementation through the interface? For concrete instances, they should have concrete implementation functions that can be called normally. But considering the implementation of dyn
, and that C++ cannot generate variable/function names through templates, I thought about having the interface class carry its own delegate
implementation, which calls different implementations based on different situations.
template<typename T>
struct Speak {
auto speak() -> std::string { return M::template call<0>(this); }
private:
using M = TraitMeta<Speak, T>;
friend M;
template<typename F>
static consteval auto collect() {
return TraitApi { &F::speak };
}
};
template call<X>
: Looks up the function in thevtable
and then calls it. Forvtable
, static dispatch referencesconstexpr static
variables, while dynamic dispatch references through pointers.collect
: Helps to get the type and address of the interface without reflection. Since it’s a template, it can also be used to verify if the interface is correctly implemented.TraitApi
: Internally, it’s a simpletuple
that stores type information and addresses.
Interface Implementation
Common Customization Points
in c++
include overloading, template specialization, Policy, and ADL.
Here we choose template specialization, which is similar to how rust
implements it.
For example, the Orphan Rule
, which is the orphan principle for trait
:
- Implement external Trait for your own types
- Implement your own Trait for external types
From the perspective of c++
, it’s easy to understand that these two rules ensure that the implementation of the Trait (i.e., template specialization) is visible to the compilation unit that references it (i.e., the .cpp/.cc
that references the .h
).
This avoids generating different implementations in different compilation units.
struct Dog;
template<>
struct Impl<Speak, Dog> {
static auto speak(TraitPtr self) -> std::string;
};
struct Dog : Speak<Dog> { std::string voice {"Woof!"}; };
auto Impl<Speak, Dog>::speak(TraitPtr self) -> std::string {
return self.as_ref<Dog>().voice;
}
...
Impl
accepts a Trait template and a concrete type.: Speak<Dog>
non-virtual inheritance allows theDog
class to have concrete interface functions, i.e.,Dog().speak()
.Impl<Speak, Dog>::speak(Dog())
directly calls the implementation.
It’s better to separate the fields
definition of Dog
.
This way, you can directly operate on Dog fields
even when Dog
is not yet defined.
struct Dog;
struct DogFields {
std::string voice {"Woof!"};
};
// Impl Speak<Dog> ...
struct Dog : DogFields, Speak<Dog> {}
Static Dispatch
Use std::semiregular
to determine if Impl
is fully defined, or you can write a template to check the size
.
template<typename A, template<typename> class... T>
concept Implemented = (std::semiregular<Impl<T, A>> && ...);
// ...
template<typename T>
requires Implemented<T, Speak>
void make_speak(T& animal) {
std::print("{}", animal.speak());
}
dyn Fat Pointer
It stores a vtable
pointer and a self
pointer.
Then use the dispatch functionality of the interface class to make the call.
template<template<typename> class Tr, ConstNess Cn>
class Dyn : public Tr<DynImpl<Tr>> {
using M = TraitMeta<Tr, DynImpl<Tr>>;
friend M;
using ptr_t = std::conditional_t<Cn == ConstNess::Const, const TraitPtr, TraitPtr>;
const decltype(M::apis)* const apis;
ptr_t self;
...
}
// ...
Dog dog;
auto dyn = make_dyn<Speak>(dog);
std::print("{}", dyn.speak());
Tr
: ATrait
interfaceTr<DynImpl<Tr>>
: Uses theDynImpl
tag to markTr
, generating concrete call functionsapis
:vtable
pointerCn
:Tr
cannot have aconst
tag, so an additional parameter is needed to markConstNess
Box dyn
TODO