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:
// Can hold either an `int`, a `float` or a `double`.
using vnum = vr::variant<int, float, double>;
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".
{0};
vnum v0::visit(vnum_printer{}, v0);
vr
// Prints "5f".
= 5.f;
v0 ::visit(vnum_printer{}, v0);
vr
// Prints "33.51d".
= 33.51;
v0 ::visit(vnum_printer{}, v0); vr
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!".
(0);
x
// Prints "float!".
(0.f); x
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"; }
);
::variant<int, float> my_variant{0};
vr
// Prints "int!".
::visit(my_visitor, my_variant);
vr
// Prints "float!".
= 5.f;
my_variant ::visit(my_visitor, my_variant); vr
(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(
("get_users", some_endpoint),
send_http_request[](const response_success& x)
{
<< "Successfully received response:\n"
cout << "\tMessage: " << x._m << "\n";
(x._p.as<std::vector<User>>());
update_user_list},
[](const response_failure& x)
{
<< "Request failure:\n"
cerr << "\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!