visiting variants using lambdas - part 1
While discussing upcoming C++17 features with other attendees at CppCon 2016, I was surprised to hear complaints about the fact that std::variant
visitation requires an external callable object.
Even though std::visit
requires an overloaded callable object as its first argument, it is possible to build such an object locally in the call site: this can easily be achieved by implementing something similar to std::overload
, proposed in P0051R2.
The aforementioned task however becomes trivial when using boost::hana
.
In this article, we’re gonna take a look at:
“Traditional” variant visitation.
“Lambda-based” variant visitation using
boost::hana
.“Fire-and-forget” variant visitation.
Future improvements/considerations.
boost::variant
or std::variant
?
Before looking at visitation techniques, I just wanted to mention that everything written in this article applies both to boost::variant
and std::variant
.
In fact, I’ve written a very simple wrapper for the upcoming examples that conditionally aliases vr::variant<Ts...>
to std::variant<Ts...>
if available, otherwise to boost::variant<Ts...>
.
The vr::visit(xs...)
function is similarly an alias for std::visit(xs...)
if available, otherwise for boost::apply_visitor(xs...)
.
All the code snippets in this article are therefore compliant to the C++14 standard.
“Traditional” visitation
Visiting a variant is usually done by writing a visitor struct
/class
outside of the scope where the variant is actually being visited. The visitor contains a set of overloads that will match the types a variant can hold.
As an example, let’s say that we have a variant type alias vnum
that can hold one of several numerical types:
We want to to visit this variant and print…
"$i"
for integers."$f"
for floats."$d"
for doubles.
…where $
is the currently stored value.
To achieve that, it’s sufficient to write a vnum_printer
struct with an operator()
overload for every type supported by our variant:
struct vnum_printer
{
void operator()(int x) { cout << x << "i\n"; }
void operator()(float x) { cout << x << "f\n"; }
void operator()(double x) { cout << x << "d\n"; }
};
Afterwards, we can simply invoke vr::visit
using an instance of vnum_printer
as the first argument, and an instance of the vnum
variant as the second one:
// Prints "0i".
vnum v0{0};
vr::visit(vnum_printer{}, v0);
// Prints "5f".
v0 = 5.f;
vr::visit(vnum_printer{}, v0);
// Prints "33.51d".
v0 = 33.51;
vr::visit(vnum_printer{}, v0);
This works, but requires us to define a new vnum_printer
visitor type - this boilerplate can be avoided.
(You can find a similar example on GitHub.)
How does vr::visit
work?
You might be asking…
How does
vr::visit
work?
The idea is very simple: by providing an overloaded callable object to vr::visit
, it can internally do something like this:
// Warning: PSEUDOCODE!
// The code below is just an example for `vnum`.
using vnum = vr::variant<int, float, double>;
template <typename TVisitor>
auto visit(TVisitor visitor, vnum variant)
{
// Everything in here would be generated by the compiler...
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.");
}
}
As you can see from the pseudocode above, all we need is a way of generating a set of overloads in order to build a valid visitor.
“Lambda-based” visitation
So, our task is to create an overload set from a variadic number of lambdas.
How can we do that?
We need a variadic make_overload(...)
template function that takes any number of callable objects as input and returns a single callable object as its output.
The returned callable object will simply be an overload of all the input callable objects. Example:
auto x = make_overload
(
[](int y){ cout << "int!\n"; },
[](float y){ cout << "float!\n"; }
);
// Prints "int!".
x(0);
// Prints "float!".
x(0.f);
Covering the implementation of make_overload
is out of the scope of this article (but an interesting idea for a future one). Therefore, you have a few options:
Implement your own
make_overload(...)
function using online resources (e.g. thestd::overload
proposal).Copy-paste my
vrm::core::make_overload(...)
implementation.Use
boost::hana::overload
, a well-tested production-ready solution. This is the approach we’re going to use for the article.
Once you get hold of a C++14-compliant compiler and #include <boost/hana.hpp>
… you’re pretty much done!
An overloaded callable object supporting a variant’s types is a valid visitor. Here’s a minimal example:
auto my_visitor = boost::hana::overload
(
[](int){ std::cout << "int!\n"; },
[](float){ std::cout << "float!\n"; }
);
vr::variant<int, float> my_variant{0};
// Prints "int!".
vr::visit(my_visitor, my_variant);
// Prints "float!".
my_variant = 5.f;
vr::visit(my_visitor, my_variant);
(You can find a similar example on GitHub.)
“Fire-and-forget” visitation
Sometimes you might want to visit a single variant with a “fire-and-forget” anonymous overload set of lambdas. That’s very easy to implement, as well. We’ll create a function that takes a variant as its first argument, then any number 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)
);
}
(Note: std::forward
needs to be used in order to perfectly-forward variant
and visitors
, which are “forwarding references”).
visit_in_place
can be used as follows:
struct response_success
{
payload _p;
message _m;
};
struct response_failure
{
message _m;
};
// ...
using http_response = vr::variant<response_success, response_failure>;
http_response send_http_request(/* ... */);
// ...
visit_in_place
(
send_http_request("get_users", some_endpoint),
[](const response_success& x)
{
cout << "Successfully received response:\n"
<< "\tMessage: " << x._m << "\n";
update_user_list(x._p.as<std::vector<User>>());
},
[](const response_failure& x)
{
cerr << "Request failure:\n"
<< "\tError: " << x._m << "\n";
}
);
As you can see from the example, visit_in_place
is useful for very specific variant visits, where the logic is not reused and all boilerplate can be easily avoided.
Final thoughts, future improvements
“Lambda-based” visitation is a very elegant way of visiting variants with minimal boilerplate. “Fire-and-forget” visitation is even better when dealing with one-time specific variant visits.
Both of them, however, do not play nicely when a stateful visitor is required. In that case, writing a struct
/class
is probably the cleanest option. Making the visitor a local type or hiding it behind an implementation namespace
is a good idea to keep it as local as possible.
Throughout the article we also did not cover recursive variant visitation: that will be the topic for my next article. It is possible to build a “lambda-based” and “fire-and-forget” visitation function for recursive variants as well, avoiding any extra overhead (e.g. std::function
is not going to be used).
Something else that could be interesting to explore is linear overloading, provided by boost::hana::overload_linearly
. This kind of overloading calls the first matching function in linear order - using this alongside SFINAE and generic lambdas could allow the creation of a visitor that uses a single function for a group of types sharing a common interface, or of a visitor that has a “default” case at the end (useful when you want to ignore some types). That will also be covered in a future article.
Thanks for reading!