Visiting Variants Using Lambdas – Part 1

notepad

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.