Even though std::visit
requires an overloaded callable object as its first argument, it is possible to create such an object directly at the call site. This can be done using a mechanism similar to std::overload
, as proposed in P0051R2.
This task becomes trivial when using boost::hana
.
In this article, we will explore:
- “Traditional” variant visitation.
- “Lambda-based” variant visitation using
boost::hana
. - “Fire-and-forget” variant visitation.
- Future improvements and considerations.
Boost.Variant or std::variant?
Before diving into visitation techniques, it is important to note that everything discussed in this article applies to both boost::variant
and std::variant
.
For convenience, I have created a simple wrapper that conditionally aliases vr::variant<Ts...>
to std::variant<Ts...>
if available; otherwise, it defaults to boost::variant<Ts...>
.
Similarly, the vr::visit(xs...)
function acts as an alias for std::visit(xs...)
if available, otherwise using boost::apply_visitor(xs...)
.
All code snippets in this article are compliant with the C++14 standard.
Traditional Visitation
Visiting a variant typically involves defining a visitor struct/class outside the scope where the variant is being visited. The visitor must contain a set of overloads that match all possible types the variant can hold.
For example, consider a variant
type alias vnum
that can hold an int
, float
, or double
:
using vnum = vr::variant<int, float, double>;
We want to visit this variant and print:
"$i"
for integers."$f"
for floats."$d"
for doubles.
A simple visitor struct can achieve this:
struct vnum_printer {
void operator()(int x) { std::cout << x << "i\n"; }
void operator()(float x) { std::cout << x << "f\n"; }
void operator()(double x) { std::cout << x << "d\n"; }
};
Now, we can invoke vr::visit
using an instance of vnum_printer
:
vnum v0{0};
vr::visit(vnum_printer{}, v0); // Prints "0i"
v0 = 5.f;
vr::visit(vnum_printer{}, v0); // Prints "5f"
v0 = 33.51;
vr::visit(vnum_printer{}, v0); // Prints "33.51d"
This method works but requires defining a separate vnum_printer
visitor. This boilerplate can be avoided.
How Does vr::visit Work?
You might be wondering how vr::visit
functions internally.
The idea is simple: by passing an overloaded callable object, vr::visit
internally checks the active variant type and invokes the corresponding overload.
Here’s an example using pseudocode:
template <typename TVisitor>
auto visit(TVisitor visitor, vnum variant) {
if constexpr(variant.index() == 0) {
return visitor(std::get<int>(variant));
} else if constexpr(variant.index() == 1) {
return visitor(std::get<float>(variant));
} else if constexpr(variant.index() == 2) {
return visitor(std::get<double>(variant));
} else {
static_assert(false, "Invalid variant state.");
}
}
To simplify this, we need a way to generate an overload set dynamically.
Lambda-Based Visitation
Our goal is to create an overload set from multiple lambdas. We need a make_overload(...)
template function that takes several callable objects and returns a single callable object.
For example:
auto x = make_overload(
[](int y){ std::cout << "int!\n"; },
[](float y){ std::cout << "float!\n"; }
);
x(0); // Prints "int!"
x(0.f); // Prints "float!"
Instead of implementing make_overload
manually, we can use boost::hana::overload
, a well-tested, production-ready solution:
#include <boost/hana.hpp>
auto my_visitor = boost::hana::overload(
[](int){ std::cout << "int!\n"; },
[](float){ std::cout << "float!\n"; }
);
vr::variant<int, float> my_variant{0};
vr::visit(my_visitor, my_variant); // Prints "int!"
my_variant = 5.f;
vr::visit(my_visitor, my_variant); // Prints "float!"
Fire-and-Forget Visitation
Sometimes, you may want to visit a variant using an anonymous overload set of lambdas. We can achieve this by creating a function that takes a variant and a set of callable objects:
template <typename TVariant, typename... TVisitors>
auto visit_in_place(TVariant&& variant, TVisitors&&... visitors) {
return vr::visit(
boost::hana::overload(std::forward<TVisitors>(visitors)...),
std::forward<TVariant>(variant)
);
}
Now, we can use visit_in_place
for handling different variant states inline:
using http_response = vr::variant<response_success, response_failure>;
visit_in_place(
send_http_request("get_users", some_endpoint),
[](const response_success& x) {
std::cout << "Received response: " << x._m << "\n";
update_user_list(x._p.as<std::vector<User>>());
},
[](const response_failure& x) {
std::cerr << "Request failed: " << x._m << "\n";
}
);
This approach is useful when dealing with one-time variant visits, eliminating unnecessary boilerplate.
Final Thoughts and Future Improvements
Lambda-based visitation is a concise way to visit variants while minimizing boilerplate. Fire-and-forget visitation further improves convenience for one-off variant visits.
However, when a stateful visitor is required, defining a struct/class remains the cleanest solution.