Rust-like traits in C++

As mentioned in my post on uniform call syntax in C++, I have become envious of traits in Rust and would like us to have something similar in C++. The great thing about traits in Rust is that you can keep the data separate from the implementation. For instance, the data needed to represent a 3D vector Vector3 can be kept as a struct, while traits of the vector, such as its length, are implemented separetely.

Note: This post is still a draft and might change in the future.

Furthermore, the traits can be defined in general, so that multiple types of data, such as a Circle and a Rectangle can share the same trait, HasArea, without being of the same type. This makes it easy to extend with both new types and new traits. Here is an example implementation of Vector3 and Length in Rust:

struct Vector3 {
    x: f64,
    y: f64,
    z: f64
}

trait Length {
    fn length(&self) -> f64;
}

impl Length for Vector3 {
    fn length(&self) -> f64 {
        return (self.x*self.x + self.y+self.y + self.z+self.z).sqrt();
    }
}

fn main() {
    let a = Vector3{x: 12.0, y: 4.0, z: 2.0};
    println!("Length is {}", a.length());
}

With inheritance in C++, it is easy to introduce new types, but hard to extend with new functionality (all existing types will need to implement any new functions). Further, it is hard to add functionality after-the-fact. If a library has defined length() as a function of Vector3, but you need lengthSquared() for performance reasons, there is no easy way to add this to the Vector3 class. You will typically have to make lengthSquared(Vector3 v) a free function, which makes it awkward, because you are now calling v.length() for the length, but lengthSquared(v) for the length squared.

After playing around with the ideas presented in Sean Parent’s NDC talk, I realized that we can make Rust-like traits happen in C++.

In the end, it allows us to do things like this:

#include "trait.h"
#include "circle.h"
#include "rectangle.h"
#include "circle_shape.h"
#include "rectangle_shape.h"
#include "shape.h"

using namespace std;

int main()
{
    vector<Shape> geometries;
    geometries.emplace_back(Circle{8});
    geometries.emplace_back(Rectangle{6});

    for (const auto &shape : geometries) {
        cout << shape.area() << endl;
    }
    return 0;
}

Here, the shapes themselves are just simple structs:

// circle.h
struct Circle
{
    const double radius = 4.0;
};
// rectangle.h
struct Rectangle
{
    const double width = 2.0;
    const double height = 3.0;
};

While the definition of the shape inherits from a Trait class that we’ll define later:

// shape.h
#include "trait.h"

struct Shape : public Trait
{
    template<typename T>
    Shape(T t)
        : Trait(std::move(t))
        , area([this](){ return ShapeImpl::area(cast<T>()); })
    {
    }

    std::function<double()> area;
};

The implementation of the area function for each shape is the straightforward implementation you would do for a free function.

// circle_shape.h
namespace ShapeImpl {
double area(const Circle &circle);
}

// rectangle_shape.h
namespace ShapeImpl {
double area(const Rectangle &rectangle);
}

// circle_shape.cpp
namespace ShapeImpl {
double area(const Circle &circle)
{
    double r = circle.radius;
    return 3.14 * r * r;
}
}

// rectangle_shape.cpp
namespace ShapeImpl {
double area(const Rectangle &rectangle)
{
    return rectangle.width * rectangle.height;
}
}

Notice how we put it inside the ShapeImpl namespace to match the implementation of Shape above. We could have used the global namespace, but we’re better off by not polluting that.

All that remains is to include trait.h, which makes this all possible. This only needs to be defined once, and can be used to create many different Trait objects like the Shape we defined above:

#include <functional>
#include <iostream>
#include <memory>

using namespace std;

struct Trait
{
public:
    Trait(const Trait &trait) = default;
    Trait(Trait &&trait) = default;
    Trait &operator=(const Trait &trait) = default;
    Trait &operator=(Trait &&trait) = default;
    virtual ~Trait() = default;

    template<typename T>
    explicit Trait(T t)
        : container(std::make_shared<Model<T>>(std::move(t)))
    {
    }

    template<typename T>
    T cast()
    {
        auto typed_container = std::static_pointer_cast<const Model<T>>(container);
        return typed_container->m_data;
    }

private:
    struct Concept
    {
        Concept() = default;
        Concept(const Concept &concept) = default;
        Concept(Concept &&concept) = default;
        Concept &operator=(const Concept &concept) = default;
        Concept &operator=(Concept &&concept) = default;
        virtual ~Concept() = default;
    };

    template<typename T>
    struct Model : public Concept
    {
        Model(T x)
            : m_data(move(x))
        { }
        T m_data;
    };

    std::shared_ptr<const Concept> container;
};

In an update of this post, I will try to extend this Trait class so that we can easily convert from one Trait to another. I.e. to allow us to go from a class Shape : public Trait to a class Length : public Trait and still keep the data contained in the first.

And after that, I’ll consider extending it to become a copy-on-write container so that the contained data can be modified, but in a way that allows us to keep value semantics.