Maps of any in C++
Among the additions and innovations that C++17 brought about, std::any is certainly a powerful one! A while back, I decided to leverage this class to create a powerful construct: a map of any. Here are the rules of the engagement.
Requirements
“a python map, in C++”.
The requirements for the exercise can be expressed as follows:
- The class had to be able to store any types: int, double, char[], std::string, user defined classes, and so on.
- The class had to be able to store N values in subsections of depth D, with N and D defined at runtime.
- The class had to preserve the interface of a C++ map. In fact, a C++ map could be thought of as a special case of the class itself.
The easiest way to think about it is as “a Python map in C++.” Feel free to try to code this class yourself. It is a fun exercise, I assure you.
False starts
“Variants cannot hold vectors, maps, class references, nor void * pointers from hell”
When I began coding a solution for this exercise, I hit a few false starts.
Initially, I was hoping I could resolve the exercise with variadic templates. Indeed, templates make for an elegant solution when we intend to use a class or function that holds the same logic for a variety of types. But they are not magic; the compiler simply creates the specialized code for each class or function at compile time. A map of strings and T can only contain the type T, whatever T might be. Additionally, we are required to define the depth of our class at compile time as well (a breach of the second requirement). Templates would just not work.
I then attempted to use another C++17 innovation: variants. Variants are special structures, similar to unions but much safer and less dorky. A variant can contain any type of primitives. A map of variants would satisfy the first requirement, allowing us to store different types side by side. But again, variants would fail miserably with the second requirement. They cannot hold vectors, maps, class references, nor void * pointers from hell. No hacky solution would make them work.
std::any comes to the rescue
Enter std::any, a class that allows safe type-casting and can store any type. So could we just get away with a map of any? Not quite.
I needed to be able to store values at arbitrary depths, which meant that I needed to create what looked like “maps inside a map” on the fly. Finally, I did not want for the class to simply return std::any. It should have handled implicit conversions, so for instance:
int my_value = anyObject["section"]["int var stored"];
I expected the anyObject object to be smart enough to return an int. I called this implicit return.
Finally, the interface had to mimic a std::map (as seen in the previous example). This meant no explicit setter and getter methods, but rather implicit overloaded operators to discern between assignments and returns.
Solution
The solution I created consists of a user defined class (called Node in my Github example). This class contains a member called storage:
std::map<std::string, std::any> storage
I overload the operator[] to create an accessor. Accessors are consumed in the process of “accessing” a member and always return a pointer to an object of type Node.
First, though, some housekeeping. I made sure to remember the key I used to access the element. This will become important for the overloaded operators that act as setters and getters.
You might notice that there is only one special case: if the element I am looking for exists and can be cast to a shared_pointer of Node. If so, I return it. Otherwise, I return the object itself (“this”).
Now the kicker, the class must be able discern if a value is being set:
anyObject["section"]["value"] = 42;
Or if it is getting queried:
int answer = anyObject["section"]["value"];
To do so, I again overload the operator= and the templated operator T(). I can assign or return the requested value:
void operator=(std::any value)
template<typename T> operator T()
But which key to use? The attentive readed will have noticed that no key is passed in with these operator.
Not to worry, the last key that was passed in with the operator[] was recoded inside the class. Voila!
Notes and additions
I am planning on expanding the example with dynamic subsection creation and the ability to use the class as an iterator, thus truly fulfilling the promise of a map like interface.
You can find the unabridged code for this exercise on my github.