An experiment to implement Rust like thick pointers in c++.
Rust and C++ differs in how dynamic dispatch is handled. In c++ each class
with virtual functions defined on the heap get a pointer to its virtual table
that keeps track of what function is associated with that instance.
In rust the pointer itself contains a pointer to the table containing the
virtual functions. That is where the name thick-pointers comes from.
Both methods have their pros and cons, for this experiment I wanted to know wether it would be possible to implement thick pointers in c++ in a way that would make them practically usable.
- There should not be more redirects than Rust-code in the function calls
- The size of a thick pointer should only be that of two normal pointers
- Each Trait/class combination should only have one function table in memory
It was possible to implement the technology behind thick pointers, but because of limitations in how c++ generates code I could not find a way to create the Trait definitions (that is the part that the programmer will use) without using Macros. This will probably not change any time soon, but if you are willing to clutter your code with macros it is now possible to use rust-like dynamic dispatching without using virtual functions.
It is also probably possible to do more macro magick to have a single trait function, but that is more than I want to mess around with macros.
Create a trait, that defines a set of functions that you want to call
Trait3(Movable, //
(move, void, (int, int), ), //
(jump, void, (bool), ), //
(speed, int, (), const), //
);The comment markers at the end of the line is to force clang-format to do line breaks.
After that define one or more classes that implements your functions:
class Apa {
public:
void move(int x, int y) {
std::cout << "apa moves " << x << ", " << y << std::endl;
};
void jump(bool x) {
std::cout << "apa jumps" << std::endl;
}
int speed() const {
return 10;
}
};
class Bepa {
public:
// Add another implementation
// void move(int x, int y) { ... }
// void jump(bool x) { ... }
int speed() const {
return 20;
}
};Notice how none of these functions are virtual.
Then start using the traits. Notice how you do not need to define that the trait
Movable and the classes Apa and Bepa relates to each other. This is done
with templating, kind of similar to how Golang does.
int main(int argc, char *argv[]) {
auto apa = Apa{};
auto bepa = Bepa{};
auto p = Movable{&apa};
p.move(10, 20);
p.jump(true);
std::cout << "current speed" << p.speed() << std::endl;
p = &bepa; // Reassign and use trait that uses Bepas functions instead
p.move(10, 30);
p.jump(false);
std::cout << "current speed" << p.speed() << std::endl;;
}Example output:
apa moves 10, 20
apa jumps
current speed 10
bepa moves 10, 30
bepa jumps
current speed 20
You could also send the pointer to functions if you want to do some usable dynamic dispatching with them. The size of each pointer only double of that of a raw c++ pointer.
void useMovable(Movable m) {
m.move(1, 2);
...
}Only dynamic dispatch is considered at this time, and not memory management. There is many other features that differs between c++ and rust, such as borrow- checking and much more. That is not in the scope of this project.