Variants vs Any - Part 2
“Jump into the world of std::any, and discover a few interesting things about type erasure
along the way”
Types go incognito
In last week’s article, we hinted to an even more “generic” std library creation: std::any.
However what do we mean by that and what problem does it attempt address?
To answer that, we need to take a step back and introduce the concept of type erasure
.
Ok, let’s imagine you are working with the following code: a vector of Base*, containing references to derived objects.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct Base{};
struct A : public Base{};
struct B : public Base{};
struct GenericVector
{
std::vector<Base*> _vector;
void insert(Base* e)
{
_vector.push_back(e);
}
};
GenericVector c;
c.insert(new A);
c.insert(new B);
One issue we encounter here is that by storing values in this vector, we lose information about the types of our objects being inserted. We
only know they belong to Base
or some subclass inheriting from Base
. The only way to recover this information is through downcasting or
changing approach entirely.
1
2
3
4
5
6
7
8
9
10
template<class T>
struct GenericVector
{
std::vector<T*> _vector;
void insert(T* e)
{
_vector.push_back(e);
}
};
Templates are an excellent solution because they allow us to retain information on the type. Indeed, the compiler will create a new
definition of the class for each T
used, replacing it in the process. However, we now face a new issue: our vector no longer accepts
heterogeneous objects! We cannot insert objects of type A
or B
, unless we set T=Base
. But this would cause information loss, which is
what we were trying to avoid initially.
Additionally, you might not always have a superclass to rely on, as in the case of predefined classes from other libraries or base types
(int
, float
, etc.).
Eureka! What if we erased or hid the type T
from the view of the GenericVector
, but exposed it internally to some container class?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct Concept
{
};
template<class T>
struct Model : Concept
{
Model( const T& t ) : object( t ) {}
T object;
};
struct Container
{
std::shared_ptr<Concept> object;
template< typename T >Container(const T& obj ) :
};
We can now use our Container
struct, along with a vector, to store any type we might think of! We are no longer losing any information
regarding the type but are instead hiding it, erasing it from Container
’s knowledge. To retrieve it, we just need to use casting:
1
2
3
4
5
6
7
8
std::vector<Container> v;
v.push_back(Container(5));
v.push_back(Container(std::string("TEST")));
auto containerInt = (*(int*)v[0].object.get());
auto containerString = (*(std::string*)v[1].object.get());
A standard container
Here’s where std::any
comes in. It’s a generic, templated container, just like the one we created earlier, but with the addition of safe casting.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
std::vector<std::any> vectorOfAny;
vectorOfAny.push_back(std::any(5));
vectorOfAny.push_back(std::any(std::string("TEST")));
try
{
auto anyInt = std::any_cast<int>(vectorOfAny[0]);
auto anyString = std::any_cast<std::string>(vectorOfAny[1]);
}
catch(const std::exception& e)
{
...
}
Conclusions
std::any
can be used in heterogeneous collection as we seen above, or when implementing reflection, or again when dealing with input of unknown type (void *, cough cough…) in a safe manner. Bewaware though, due to runtime type checking, software where performance is paramount might want to steer clear of this class.
I hope you found this article enjoyable and as always, you can find the code example on my github page. Happy coding!