If you’ve got a set of types, and you need to iterate over them and do something with each of them, your initial instinct would probably be to make them related. For example, if we had a set of application menu commands, we might make them all inherit from a pure virtual base class (let’s call it “MenuCommand”). The sub-classes might be “FileMenuCommand”, or “ToolsMenuCommand”. We could then simply create a vector of smart pointers to MenuCommand and iterate over that. Right?
This gives us flexibility, but when we want to do something against each MenuCommand type, what do we do? Let’s assume our design is such that we have, in the base class, a member function called “print”. We can iterate over each element of the vector and simply call MenuCommand->print().
This has several disadvantages we might want to consider. Firstly, there’s the overhead of virtual function calls. Each call to print() requires a redirect to the actual type’s print() member function. Another issue is that we are iterating over smart pointers, so again, we have to dereference the pointer to look on the heap where the actual object is stored. Also, there’s the issue that the actual objects could potentially be scattered indeterminately on the heap, which means the CPU can’t use its caching algorithms effectively. Finally, we might want to treat some MenuCommands completely differently from others – we might want to call a completely different member function, or several. Adding a “type” enum to each MenuCommand and a switch to determine type becomes a maintenance problem, and dynamic casting has similar issues.
std::visit and std::variant allows us to use the visitor pattern to avoid these concerns. Consider instead that we could use disparate types, with no inheritance. If we create a vector of std::variant<FileMenuCommand, ToolsMenuCommand> (for example), we can (a) create a vector of concrete types (so no heap allocation) and (b) we don’t have any dereferencing or virtual calls, thus improving both speed, and locality (enhancing our chances of CPU cache hits). Here’s a trivial example:
#include <iostream>
#include <variant>
#include <vector>
struct FileMenuCommand {
void print() const
{
std::cout << "File Menu\n";
}
};
struct ToolsMenuCommand {
void display() const
{
std::cout << "Tools Menu\n";
}
};
template <typename... Ts> struct overloaded : Ts... {
using Ts::operator()...;
};
int main()
{
std::vector<std::variant<FileMenuCommand, ToolsMenuCommand>> vec;
vec.emplace_back(ToolsMenuCommand {});
vec.emplace_back(FileMenuCommand {});
for (const auto& o : vec) {
std::visit(
overloaded {
[](const ToolsMenuCommand& tmc) { tmc.display(); },
[](const FileMenuCommand& fmc) { fmc.print(); },
},
o);
}
return 0;
}
You can see we have no heap allocations for the types in the vector – obviously its storage is heap-based, but the types are all local to it.
We can also tailor our calls to each type (note that we call display() for ToolsMenuCommand, but print() for FileMenuCommand – confusing, but it serves to illustrate the point that we can act differently depending on the object).
The one confusing item above might be the “overloaded” template – This is a small utility that lets us construct a single callable object from multiple lambdas, so that std::visit can dispatch to the right one based on the active type in the variant.
It inherits from all of the types we pass in (in this case, the lambdas — each lambda is its own anonymous struct type with an operator()). The using Ts::operator()...; line pulls all of those operator() overloads into the same scope, so the compiler can see them together and perform normal overload resolution.
Without it, we’d have a problem: std::visit requires a single callable, but we have two separate lambdas. overloaded merges them into one object that has both call operators, so when std::visit calls it with a ToolsMenuCommand, overload resolution picks the first lambda, and with a FileMenuCommand it picks the second.







