std::any aka variable variables
While std::any
is not a new addition to the standard library, it was added back in 2017, I only started to take
advantage of it from like a year ago, mostly because I was solving other time consuming problems and refactoring old
code was not among the priorities at the time.
Before we dive in, you can find the complete code in my repository.
So what is std::any
?⌗
According to cppreference:
The class any describes a type-safe container for single values of any copy constructible type.
With that in mind and the fact that the class is not parameterized we are allowed to have this:
// One function that can return different type values:
std::any foo();
// One function that can take in different type arguments:
void bar(const std::any&);
// Mixed types container:
std::vector<std::any> vec{"string", 3, 3.2};
Pretty neat! So with this abstraction we can also achieve some form of type erasure, up to a point.
Should we attempt to build something like this?⌗
We would need some form of type erasure to allow mixing std::any
s of different value types. So we can’t parameterize the whole
class for that, but we can define template constructors to take care of it:
class Any {
public:
template <typename T>
Any(T&& value);
};
This way the compiler will generate different constructors based on the types that we wish our Any
class to hold.
What else do we need? Because our class can hold values of any type we can use a void*
to point to any instance.
But our container is type safe so when Any
’s destructor is called it must also call the proper destructor for which the
void*
points to … a generic function should do. So far we have this:
using delete_fn = void(void*);
template <typename T>
static void delete_help(void* ptr) {
if (ptr)
delete static_cast<T*>(ptr);
}
class Any {
public:
template <typename T>
Any(T&& value) {
delete_helper_ = &delete_help<T>;
ptr_ = new T(std::forward<T>(value));
}
~Any() {
delete_state();
}
private:
void delete_state() {
if (delete_helper_) {
delete_helper_(ptr_);
}
}
private:
delete_fn* delete_helper_{nullptr};
void* ptr_ = nullptr;
};
You might ask, but how will this work if delete_help(void*)
is parameterized? Well, it doesn’t have any parameterized parameters,
the type is used internally so it’s not part of the function signature, which means that once we decay it to a function pointer it
should look exactly like the delete_fn
alias.
You will now say that the specification allows us to retrieve the contained value and will also throw if the requested type doesn’t match the one contained. How can we achieve the same?
class Any {
struct __non_existing_any {};
public:
Any()
: type_info_(typeid(__non_existing_any)) {
}
template <typename T>
Any(T&& value)
: type_info_(typeid(T)) {
delete_helper_ = &delete_help<T>;
ptr_ = new T(std::forward<T>(value));
}
private:
std::type_index type_info_;
// more members follow ...
};
So all we did was to add some information about the type contained via typeid
when we invoke a constructor. This allows us to
indirectly know something about the type we’re managing.
With this added we can easily retrieve the managed value:
template <typename T>
T& value() {
if (!ptr_)
throw std::runtime_error("no object contained");
if (type_info_ != typeid(T))
throw std::runtime_error("bad any cast");
return *(static_cast<T*>(ptr_));
}
That’s it?⌗
Overall this is all we would need to achieve a rudimentary form of std::any
. Something similar to what std::any
achieves in matters
of type erasure is actually std::shared_ptr<void>
:
auto shared_tmp = std::make_shared<int>(3);
std::shared_ptr<void> shared_void = shared_tmp;
// use shared_void to actually manage the resource
It achieves the same thing by exploiting template constructors and proper deleter propagation when doing copies. But as you can see this
is not as flexible as std::any
when it comes to accessing the actual value, you have to come up with your own accessors while
std::any
already provides that for you.
But you use a void*
as a handle, is that efficient?⌗
This is something that we didn’t cover in our examples. Official implementations usually select at compile time how they want to
store the value, so for smaller types std::any
might actually use a small internal buffer and only manage via pointers larger types.
See placement new and think how you could modify the code to do
this optimization.
For this exact reason I don’t recommend having a container full of std::any
s. First would be performance because some types
may be much larger than the small buffer thus you can’t take much advantage of cache locality and second would be iteration for
which you need to define some kind of a map of conversions to properly handle the types which can be quite verbose.
Why would you want to use it?⌗
IMHO std::any
is great for callbacks, because you can define only one callback and then take care of the different values in that
callback’s definition. To some extent sometimes it may be usefull to mix different types inside a container, as long as you don’t
iterate through it too often. But these are not all the scenarios where you might need a “generic variable”.
My organization doesn’t use C++17 yet, what can I do?⌗
Honestly that sucks. But you can use Boost.Any
instead, if that’s also not possible I guess you could attempt to roll your own but
be aware that my example has been kept simple for demonstration purposes, it doesn’t have the “small type” optimization, the interface
is not that safe as the official ones etc.
I also recommend having a look at how both GCC and clang implement this abstraction, it’s a bit harder to read due to all safety checks that they perform and overall the coding style that they use but you should learn some new tricks from just having a read.