One of the very few features of the C++ standard library that I unapologetically like, is the std::visit. It reduces the amount of boilerplate code, kills multiple birds with a single stone, and doesn’t sacrifice performance while doing it. So let’s have a look at how to use it.
Visitor pattern
The Visitor pattern is a more complex OOP design pattern, but once you understand it, it becomes a great tool for breaking up dependencies and removing obtuse conditional statements with RTTI.
It allows you to separate data structures from algorithms operating on those data structures, making it easier to add new algorithms or alter the structures without having to redeploy the other half.
It is popular in compilers, where your source code is represented by an abstract syntax tree (AST), and every single optimization or analysis, like constant propagation, expression folding, or precondition analysis is implemented as its own class (so-called Visitor), separately from the AST declaration.
If you’d like to know how to implement Visitor from the ground up, look no further than superb refactoring.guru website.
Without inheritance
Unlike the standard object-oriented implementation of the Visitor pattern, std::visit doesn’t require you to use inheritance. Due to the underlying use of std::variant and dark standard library magic, std::visit doesn’t use double dispatch and thus you don’t need any accept method on your data structures:
#include <iostream>
#include <variant>
#include <vector>
struct NodeA {};
struct NodeB {};
struct NodeC {};
using Nodes = std::variant<NodeA, NodeB, NodeC>;
struct Visitor {
void operator() (const NodeA&) { std::cout << "NodeA" << std::endl; }
void operator() (const NodeB&) { std::cout << "NodeB" << std::endl; }
void operator() (const NodeC&) { std::cout << "NodeC" << std::endl; }
};
int main() {
std::vector<Nodes> nodes = {
NodeA{},
NodeB{},
NodeA{},
NodeC{}
};
for (auto&& node : nodes) std::visit(Visitor{}, node);
// Prints:
// NodeA
// NodeB
// NodeA
// NodeC
}
As you can see, all you need is a callable struct with overloads for required types. Note that you need to overload on all types that belong to Nodes or you’ll get a compiler error (like with pattern matching in other languages).
The error itself is a classic std nonsense, so look out for this message:
no type named 'type' in 'std::invoke_result<Visitor, TypeThatIsMissing&>'
With inheritance
However, std::visit allows using inheritance. That is even darker magic because you can, but don’t have to, add overloads for only some of the inherited types. The algorithm needs to be able to match the type on at least one overload, and the base class also applies:
#include <iostream>
#include <variant>
#include <vector>
struct NodeBase {};
struct NodeA : public NodeBase{};
struct NodeB : public NodeBase{};
using Nodes = std::variant<NodeA, NodeB>;
struct Visitor {
void operator() (const NodeA&) { std::cout << "NodeA" << std::endl; }
void operator() (const NodeBase&) { std::cout << "NodeBase" << std::endl; }
};
int main()
{
std::vector<Nodes> nodes = {
NodeA{},
NodeB{},
};
for (auto&& node : nodes) std::visit(Visitor{}, node);
// Prints:
// NodeA
// NodeBase
}
The order of overloads doesn’t matter here. Specialization is preferred over the base class.
Faster than manual dispatch
While std::visit surely has a nice interface (a rare sight in the standard library) and it requires less boilerplate than implementing double dispatch manually, how’s the performance? I do have an unexpected answer - it is faster or at least as fast as the other solutions!
I highly recommend watching Klaus Ingleberger’s talk about the Visitor pattern implementations where he also shows comparison times on GCC and Clang (timestamped to the part with the performance comparison). My own benchmarks on MSVC way back showed a clear win for std::visit over other methods.
Functional programming
C++ committee often comments on how they like their proposals to kill multiple birds with one stone and in the notion of that mindset, you can use std::visit functionally as well.
#include <iostream>
#include <variant>
#include <vector>
struct NodeA {};
struct NodeB {};
using Nodes = std::variant<NodeA, NodeB>;
template<class... Ts>
struct overloaded : Ts... { using Ts::operator()...; };
// Some compilers might require this explicit deduction guide
template<class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;
int main()
{
std::vector<Nodes> nodes = {
NodeA{},
NodeB{},
};
for (auto&& node : nodes) {
std::visit(overloaded{
[] (const NodeA&) { std::cout << "NodeA" << std::endl; },
[] (const NodeB&) { std::cout << "NodeB" << std::endl; }
}, node);
}
// Prints:
// NodeA
// NodeB
}
Instead of creating one dedicated visitor struct, you can use a helper template class to create an object out of a set of lambdas. This way, you can define your visiting code in the same place as you call it. Some people use this trick to emulate match statements from other languages, others might like it simply because it is functional programming.
Disadvantages
As noted by Klaus in his talk, every approach has its advantages and disadvantages. Because this approach is driven by the std::variant, you must keep in mind its shortcomings:
- Types need to be fully defined before they can be used in the variant. This is because their storage size needs to be known.
- Using types with vastly different sizes means that your variant will inherently waste a lot of space.
- Adding a new type might mean updating all existing visitors (not if you use inheritance) and updating the variant (which you don’t have to do with the classic OO approach).
Do note that you can address the first two problems by using wrapper types like smart pointers or employing the Pimpl pattern.
Summary
As I said in the beginning, I like std::visit very much. Implementing the Visitor pattern takes some getting used to and with this feature, you greatly reduce the amount of boilerplate, improve the readability of the code, and don’t sacrifice performance.
Functional invocation is just a cherry on top for very localized usage where brand new class would just add noise to the code base.