Container with copy-by-value semantics

Recently, I have been inspired to explore the benefits of copy-by-value semantics by talks like Sean Parent’s “Better Code: Runtime Polymorphism”. This has lead me to experiment with a new container that behaves like a regular value, but still limits the number of copies made of the data it contains.

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

The container is a wrapper around a shared pointer that uses a copy-on-write strategy, meaning that you can make an infinite number of copies without copying the actual data. However, as soon as you modify one of the copies, that immediately copies itself before writing, to make sure the other copies remain intact. This has the benefit of copies being cheap most of the time, but has the downside that it is harder to control exactly when a real copy happens. However, if you treat the values as if they would do a full copy all the time, and try to use std::move as much as possible to signal the compiler that you are done with a given copy, you should end up with only benefits over regular values.

The container supports polymorphic types, which means that its type can be an abstract class, and it will still be able to copy the contents correctly. It basically implements for you the pattern where you would have a separate clone function.

Here is a small example that shows the container in use. We define an abstract Shape type, with two implementations: Rectangle and Circle.

struct Shape {
    Shape() = default;
    virtual void test() const = 0;
};

struct Rectangle : public Shape {
    Rectangle() = default;
    Rectangle(double width, double height)
        : width(width)
        , height(height)
    {
    }

    void test() const override
    {
        cout << "Rectangle with width " << width << " and height " << height << endl;
    }

    double width = 6;
    double height = 4;
};

struct Circle : public Shape {
    void test() const override
    {
        cout << "Circle with radius " << radius << endl;
    }

    double radius = 10;
};

int main()
{
    vector<Value<Shape>> stuff;
    stuff.push_back(Circle());
    stuff.push_back(Rectangle());
    stuff.push_back(Circle());
    stuff.push_back(Rectangle(10, 12));

    stuff[2].as<Circle>()->radius = 24;

    auto stuff2 = stuff;

    for (auto& s : stuff) {
        s->test();
    }

    for (auto& s : stuff2) {
        if (s.can_convert<Circle>())
            s.as<Circle>()->test();
    }
}

As you can see from the above example, the container also supports downcasting through the can_convert and as functions. This is similar to doing auto *circle = dynamic_cast<Circle*>(shape); with regular pointers.

The implementation of the Value container is as follows:

#include <iostream>
#include <memory>
#include <vector>
#include <sstream>

using namespace std;

struct InPlaceT {
    explicit InPlaceT() = default;
};
constexpr InPlaceT InPlace {};

template <typename T>
struct Value
{
    template<typename U>
    struct is_derived
            : public std::integral_constant<bool,
            std::is_base_of<T, std::decay_t<U>>::value>
    {};

    Value() = default;

    // Copy constructors
    template <typename U,
              typename UU = std::decay_t<U>,
              typename = std::enable_if_t<is_derived<U>::value>>
    Value(const Value<U> &other)
        : container(other.container)
        , copy_container(&Value::make_shared_helper<UU>)
    {}

    template <typename U,
              typename UU = std::decay_t<U>,
              typename = std::enable_if_t<is_derived<U>::value>>
    Value(const U& arg)
        : container(std::make_shared<UU>(arg))
        , copy_container(&Value::make_shared_helper<UU>)
    {
    }

    // Move constructors
    template <typename U,
              typename UU = std::decay_t<U>,
              typename = std::enable_if_t<is_derived<U>::value>>
    Value(Value<U> &&other)
        : container(std::move(other.container))
        , copy_container(&Value::make_shared_helper<UU>)
    {
    }

    template <typename U,
              typename UU = std::decay_t<U>,
              typename = std::enable_if_t<is_derived<U>::value>>
    Value(U &&value)
        : container(std::make_shared<UU>(std::forward<U>(value)))
        , copy_container(&Value::make_shared_helper<UU>)
    {
    }

    // Copy assignment
    template <typename U,
              typename UU = std::decay_t<U>,
              typename = std::enable_if_t<is_derived<U>::value>>
    Value &operator=(const Value<U> &other)
    {
        container = std::make_shared<UU>(*other.container);
        copy_container = &Value::make_shared_helper<UU>;
        return *this;
    }

    template <typename U,
              typename UU = std::decay_t<U>,
              typename = std::enable_if_t<is_derived<U>::value>>
    Value &operator=(const U &value)
    {
        container = std::make_shared<UU>(value);
        copy_container = &Value::make_shared_helper<UU>;
        return *this;
    }

    // Move assignment
    template <typename U,
              typename UU = std::decay_t<U>,
              typename = std::enable_if_t<is_derived<U>::value>>
    Value &operator=(Value<U> &&other)
    {
        container = std::make_shared<UU>(std::move<shared_ptr<UU>>(*other.container));
        copy_container = &Value::make_shared_helper<UU>;
        return *this;
    }

    template <typename U,
              typename UU = std::decay_t<U>,
              typename = std::enable_if_t<is_derived<U>::value>>
    Value &operator=(U &&value)
    {
        container = std::make_shared<UU>(std::forward<U>(value));
        copy_container = &Value::make_shared_helper<UU>;
        return *this;
    }

    template <typename... Args>
    Value(InPlaceT, Args&&... args)
        : container(std::make_shared<T>(std::forward<Args>(args)...))
        , copy_container(&Value::make_shared_helper<T>)
    {
    }

    template <typename U>
    bool can_convert() const
    {
        auto typed_container = std::dynamic_pointer_cast<const U>(container);
        if (typed_container == nullptr) {
            return false;
        }
        return true;
    }

    const T& operator*() const
    {
        return *container;
    }

    const T &get() const
    {
        return *container;
    }

    template<typename U>
    U* as()
    {
        if (container == nullptr) {
            return nullptr;
        }
        if (container.use_count() != 1) {
            container = copy_container(container);
        }
        auto typed_container = std::dynamic_pointer_cast<U>(container);
        if (typed_container == nullptr) {
            stringstream error_message;
            error_message << "Cannot cast from ";
            error_message << typeid(T).name();
            error_message << " to ";
            error_message << typeid(U).name();
            throw std::runtime_error(error_message.str());
        }
        return typed_container.get();
    }

    const T* operator->() const
    {
        return container.get();
    }

    T* operator->()
    {
        if (container == nullptr) {
            return nullptr;
        }
        if (container.use_count() != 1) {
            container = copy_container(container);
        }
        return container.get();
    }

    template<typename U>
    static std::shared_ptr<T> make_shared_helper(std::shared_ptr<T> c)
    {
        auto typed_container = std::static_pointer_cast<U>(c);
        return make_shared<U>(*typed_container);
    }

private:
    std::shared_ptr<T> container;

    std::function<std::shared_ptr<T>(std::shared_ptr<T>)> copy_container;

    template<typename U>
    friend struct Value;
};