std::any
std::any
to implement unconstrained dynamic types, and understand when they should be usedIn this lesson, we’ll cover the two main techniques for implementing dynamic types in C++. A variable with a dynamic type can store different types of data throughout its life. For example, it might be initialized as an int
, have a float
assigned to it later, and then finally changed to store a Player
or some other custom type.
However, we should remember that C++ is designed as a statically typed language. The use of dynamic types, particularly unconstrained dynamic types, should be quite rare.
std::variant
.There are some use cases where neither of these options apply, and we need to create an unconstrained container. Some examples include:
To deal with these scenarios, we have void pointers, or containers that are based on void pointers. An example of such a container is the standard library’s std::any
, introduced in C++17. We’ll cover both void pointers and std::any
in this lesson.
The main way C++ represents a pointer to potentially any type of data is a void pointer. The syntax looks similar to any other pointer - the type it’s pointing to is simply void
:
void* Ptr;
We assign a memory address to such a pointer in the usual way. We can assign a memory address for any data type to a void*
.
Below, our Ptr
points first at an int
, and then gets reassigned to point to a float
:
int main(){
int SomeInt{42};
void* Ptr{&SomeInt};
float SomeFloat{9.8f};
Ptr = &SomeFloat;
}
Similar to any other pointer type, void pointers can be initialized or updated to nullptr
. This is used to represent the absence of a value, which can be tested for by treating the pointer as a boolean:
#include <iostream>
int main(){
void* Ptr{nullptr};
if (!Ptr) { std::cout << "Empty"; }
}
Empty
We cannot directly dereference a void pointer. To access the value it is pointing at, we need to first cast the void pointer to match the underlying type:
#include <iostream>
int main(){
int SomeInt{42};
void* Ptr{&SomeInt};
int* IntPtr{static_cast<int*>(Ptr)};
std::cout << "Data: " << *IntPtr;
}
Data: 42
Similar to unions, which we introduced in the previous lesson, void pointers offer very little type safety.
It is on us to keep track of what type our void*
is pointing at. In a larger program, that bookkeeping can be complicated.
And most importantly, if we get it wrong, the compiler won’t tell us. Our program will simply have a bug that could have serious implications:
#include <iostream>
template <typename T>
void Log(void* Data){
T* Ptr{static_cast<T*>(Data)};
std::cout << "Data: " << *Ptr << '\n';
}
int main(){
int SomeInt{42};
void* Ptr{&SomeInt};
Log<int>(Ptr);
float SomeFloat{3.14};
Ptr = &SomeFloat;
Log<int>(Ptr);
}
Data: 42
Data: 1078523331
Because of this, using void pointers directly is quite dangerous. It’s generally recommended to wrap the concept in an intermediate container that provides some additional type safety.
In C++17, the standard library introduced an implementation of this concept: std::any
std::any
ContainersThe std::any
type is available within the <any>
header and can be constructed in the usual ways.
Below, we create a container that is initially storing a type of int
, and a value of 42
:
#include <any>
int main(){
std::any Data{42};
}
std::make_any()
We also have access to the std::make_any()
function. It receives the type we want to construct as a template argument, and a list of function arguments to forward to that type’s constructor. It then returns a std::any
initialized with that object.
#include <any>
struct Vec3 {
float x;
float y;
float z;
};
int main(){
auto Data{std::make_any<Vec3>(1.f, 2.f, 3.f)};
}
The reason for this function is similar to the rationale behind the smart pointer generators like std::make_unique
and std::make_shared
.
It mitigates some potential memory leaks with constructors that can throw exceptions. Compilers can additionally make small optimizations if the std::any
and the object it contains can be constructed at the same time.
std::any
ValuesThere are three main ways we can update the value contained within a std::any
=
The simple assignment operator, =
, works as we’d expect. Naturally, the type we’re assigning doesn’t need to match the current type the std::any
is holding:
#include <any>
int main(){
std::any Data { 42 };
Data = 3.14f;
}
emplace()
When we want to simultaneously construct and assign an object to the std::any
, we should use the emplace()
function. This has the same rationale as the emplace()
method in other container types.
Specifically, for non-trivial types, constructing an object in place is more efficient than constructing it outside of the container and then moving it in.
The std::any
's emplace()
method accepts the type we want to construct as a template argument, and the parameters to forward to that type’s constructor as function arguments:
#include <any>
struct Vec3 {
float x;
float y;
float z;
};
int main(){
std::any Data{42};
Data.emplace<Vec3>(1.f, 2.f, 3.f);
}
reset()
We can destroy the object held by a std::any
using the reset()
 method:
#include <iostream>
#include <any>
struct Vec3 {
float x;
float y;
float z;
~Vec3(){ std::cout << "Destructor"; }
};
int main(){
auto Data{std::make_any<Vec3>(1.f, 2.f, 3.f)};
Data.reset();
std::cout << "\nBye!";
}
Destructor
Bye!
std::any
ValuesSimilar to void pointers, we need to do some additional work before we can access values stored in a std::any
has_value()
std::any
can be initialized without a value, or we can delete their value using the reset()
method. The has_value()
method returns a boolean, letting us check if the container is currently holding a value:
#include <iostream>
#include <any>
int main(){
std::any Data{42};
if (Data.has_value()) {
std::cout << "I have a value";
}
Data.reset();
if (!Data.has_value()) {
std::cout << "\nbut not any more";
}
}
I have a value
but not any more
std::any_cast()
To access the value stored in a std::any
, we pass it to the std::any_cast()
function, passing the type we expect to receive as a template argument:
#include <iostream>
#include <any>
int main(){
std::any Data{42};
std::cout << "Value: "
<< std::any_cast<int>(Data);
}
Value: 42
std::bad_any_cast
ExceptionUnlike void pointers, std::any
provides type safety at this point. If the type we provide as a template argument is not the type the std::any
is currently storing, we get an exception.
Specifically, it’s a std::bad_any_cast
exception, which we can detect and react to in the usual way:
#include <iostream>
#include <any>
int main(){
std::any Data{42};
try { std::any_cast<float>(Data); }
catch (const std::bad_any_cast& e) {
std::cout << "That wasn't a float!";
}
}
That wasn't a float!
We can get the type currently being stored by a std::any
using the type()
 method.
This will be returned as a std::type_info
. We can use the typeid()
operator to compare this to other types:
#include <iostream>
#include <any>
int main(){
std::any Data{42};
if (Data.type() == typeid(int)) {
std::cout << "We have an int over here";
}
}
We have an int over here
If the container is empty, the object returned by type()
will be equal to typeid(void)
:
#include <any>
#include <iostream>
int main(){
std::any Data;
if (Data.type() == typeid(void)) {
std::cout << "It's empty";
}
}
It's empty
We covered std::type_info
and the typeid()
operator in detail in a dedicated lesson earlier in the course:
In this lesson, we introduced two ways of managing fully dynamic types in C++: void pointers and std::any
. The std::any
container provides a safer, standardized alternative to void*
, but unconstrained dynamic types should still be used sparingly
Here are the key takeaways:
void*
) allow pointing to data of any type, but provide no type safetystd::any
, introduced in C++17, is a type-safe container for single values of any typestd::any
can be created by directly specifying a value, or using std::make_any()
std::any
can be updated by assignment, emplaced with emplace<SomeType>()
, or reset with reset()
std::any_cast<SomeType>()
, which throws std::bad_any_cast
if the types mismatchstd::any
can be checked at runtime using the type()
method and compared to another type using the typeid()
operatorstd::any
Learn how to use void pointers and std::any
to implement unconstrained dynamic types, and understand when they should be used
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.