Sometimes in C++ you really need to break out of the type-system. This can be done using std::any! It can store any value; but how does it work and how can it be used? We implement it in 150 lines in learn-stl!

We will answer the following questions;

  • Whats the runtime overhead in using std::any?
  • How does it know what type its storing?
  • Would would you need std::any?

How its used?

any is a type-safe container that can store any value type at run-time and it can even be empty! Its not templated like variant.

learn::any any_value = "hello world";
any_value = 12.345f;
any_value = 23.45;
any_value = 12;

It can be accessed using any_cast. Several variants of any_cast exist, allowing it to be accessed in different ways. Each has their own performance trade-off; you might want to avoid copying or throwing exceptions!

// Cast 1- return copy of internal value
const int my_value0 = learn::any_cast<int>(any_value);

// Cast 1 - throws a std::bad_any_cast as type is bad
const double my_value1 = learn::any_cast<double>(any_value);

// Cast 2 - returns a nullptr if the type is bad! 
const int* my_ptr = learn::any_cast<int>(&any_value);

// Cast 3 - moves the value out of the any container
const int my_value2 = learn::any_cast<int>(learn::move(any_value));

If you want to check the type thats stored; you can use the type() member function.

if (!any_value.has_value()) {
    return -1;
}

if (any_value.type() == typeid(int)) {
    return learn::any_cast<int>(any_value);
}

But why would you want to use this? This replaces the void* pattern thats common in older code bases; std::any adds type safety. This is used to limit and control the number of dependencies in larger codebases and systems. This is commonly found in message passing / file parsing when handlers should be agnostic to what they are acting on.

This should be a class of last resort; optional or variant should be used instead.

How it works

But how does any do this? Internally, any has a ContainerInterface and a templated Container, which is templated on the stored value type.

struct ContainerInterface {
    using Ptr = ErasedPtr<ContainerInterface>;

    virtual ~ContainerInterface() = default;
    virtual const std::type_info& type() const = 0;

    virtual Ptr copy() const = 0;
};

template <typename ObjectT>
struct Container : ContainerInterface {
    using Object = std::decay_t<ObjectT>;

    template <typename... Args>
    static Ptr make(Args&&... args) {
        return Ptr(new Container(forward<Args>(args)...));
    }

    explicit Container(const Object& _data) : data(_data) {}
    explicit Container(Object&& _data) : data(forward<Object>(_data)) {}
    Object data;

    const std::type_info& type() const final { return typeid(data); }

    Ptr copy() const final { return make(data); }
};

We store a unique_ptr to a Container through ContainerInterface, allowing the ContainerInterface pointer to store a Container for any type. Most standard library implementations use a small-value optimization (SVO) to improve performance. This can be seen in the learn_stl implementation of any

To access the stored value, we perform a dynamic_cast to cast down the inheritance hierarchy. If the cast fails, then it returns a nullptr. Below you can see how Cast 2 is done.

template <typename Object>
const Object* any_cast(const any* value) {
    auto container_ptr = dynamic_cast<const any::Container<Object>*>(value->container_.get());

    return container_ptr ? addressof(container_ptr->data) : nullptr;
}

This function can be used to implement the other any_cast functions.