Visiting Without Travelling

This crossed my radar a few days ago and the whole lambda overloading thing is looking mighty useful. One thing I’ve always wanted to do was be able to create visitors for boost::variant in place instead of having to craft a whole new class somewhere else. With overload_set I can do just that.

First off we need to create a little machinery

template <typename Result, typename... Fs>
struct inplace_visitor : boost::static_visitor<Result>, overload_set<Fs...>
{
   inplace_visitor (Fs... fs):
      overload_set<Fs...> (std::move (fs)...)
   {}

   using overload_set<Fs...>::operator();
};

template<typename Result, typename... Fs>
inplace_visitor<Result, typename std::decay<Fs>::type...> make_visitor(Fs&&...fs)
{
   return inplace_visitor<Result, typename std::decay<Fs>::type...>{std::forward<Fs>(fs)...};
}

Visitors for boost::variant need to be derived from boost::static_visitor so we need to create a new top-level class that derives form both boost::static_visitor and overload_set. Once we’ve done that we’re all set to use it like so

auto v = boost::variant<int, double, std::string, bool>(1);
boost::apply_visitor (make_visitor<void> (
    [](int x) {std::cout << "int = " << x << std::endl;},
    [](double x) {std::cout << "Double = " << x << std::endl;},   
    [](const std::string& x) {std::cout << "String = " << x << std::endl;},
    [](auto x) {std::cout << "auto = " << x << std::endl;}),
  v);

Note that you have to specify the return type for the visitor as a template parameter to make_visitor. So if you want your visitor to return something you’d have to do something like

std::cout << boost::apply_visitor (make_visitor<int> (
    [](int) {return 1;},
    [](double) {return 2;},
    [](const std::string&) {return 3;},
    [](auto) {return 0;}),
  v1) << std::endl;

If you mess up and your lambdas don’t all have the correct return type then you’ll get to burrow into a nice, long chain of compiler diagnostics to figure out where you went wrong. I don’t have a ton of time to spend on this right now so I haven’t figured out a a way to get cleaner compiler errors using static_assert. Maybe when we’re using Visual Studio 2015 at work and can actually use this stuff (we’re currently on 2010 and don’t have access to variadic templates yet) I’ll get things cleaned up.

You can grab the source code (including a test program) here. So far I’ve tested it with Visual Studio 2015 and (apple) clang 7.0 on OSX. It worked great in both cases though clang barfed on using the function object factory version of apply_visitor1 with make_visitor, e.g. code like this

auto apply = boost::apply_visitor (make_visitor<void> (
    [](int x) {std::cout << "int = " << x << std::endl;},
    [](double x) {std::cout << "Double = " << x << std::endl;},
    [](const std::string& x) {std::cout << "String = " << x << std::endl;},
    [](auto x) {std::cout << "auto = " << x << std::endl;}));

Visual Studio handled it fine. For clang I had to convert it to

auto visitor = make_visitor<void> (
    [](int x) {std::cout << "int = " << x << std::endl;},
    [](double x) {std::cout << "Double = " << x << std::endl;},
    [](const std::string& x) {std::cout << "String = " << x << std::endl;},
    [](auto x) {std::cout << "auto = " << x << std::endl;});
auto apply = boost::apply_visitor (visitor);

I’m not sure which is correct, hopefully it’s the visual studio version.

[UPDATE] As mentioned in the comments below, the discrepancy between what Clang And Visual Studio could handle was only there because I missed a “hey, you’re using a language extension” warning from Visual Studio. Clang is doing the right thing in this case.


  1. i.e. give it only the visitor object and get a function object whose operator() visits the variant passed to it in return.

Advertisements

7 comments

  1. This should come very handy when you want to use standard algorithms to containers of boost::variant, something that resulted in a lot of scattered code until now; the only concern is the “looks” of the code appeal mostly to the “trained eye”. As for the clang issue, it seems to work for me http://coliru.stacked-crooked.com/a/d8ebdf31fdeb789c , unless that’s not what you meant.

  2. I think that if you’re using an advanced construct (for C++) like boost::variant then it’s safe to assume that the eyes seeing the code are trained well enough to figure out what make_visitor is doing (and can decode the lambda syntax). Though maybe I’m assuming too much here.

    The clang issue I was talking about is this http://coliru.stacked-crooked.com/a/c944ca648a969403. VS2015 is able to compile it, clang can’t — which is surprising as it is usually the other way around. I did manage to get an internal compiler error in VS when I working on this though (the error happened in code that wouldn’t have compiled anyways, just got an ICE instead of an error message).

  3. As per the compilation error in clang, it’s due to the overloads provided by apply_visitor (nothing to do with overload_set per se) : the only viable candidate (the one argument version) accepts a Visitor& and a temporary can’t bind to a non const reference. You’ll also see this comilation error in VS if you disable the language extensions (/Za in compiler options) :

    >error C2664: ….
    >…
    >note: A non-const reference may only be bound to an lvalue

    I imagine the same extension allows compilation in gcc as well. A “dirty” fix would be to use a cast since the function call operators of the lambdas are const member functions (unless you had mutable lambdas) and we know we won’t be modifying the temporary object :

    template<class T>
    T& lval(T&& val)
    {
        return val; 
    }
    

    and this compiles : http://coliru.stacked-crooked.com/a/7ba5513f812e167f

  4. Yep, somehow I missed the warning about the “temporary and non-const ref” when I originally was building with VS2015. Using the project files generated by cmake I get an error since those projects have the “treats warnings as errors” switch turned on.

    Really, we’re not losing anything with this error since “boost::apply_visitor (test_visitor())” (where “test_visitor” is an old-style visitor class) will also fail to compile with the same error.

  5. I recently wrote something similar, but with a few differences that might be worth sharing:

    – My version is centered around a visit(variant, overloads…) function, because I wanted to maximize syntactic convenience for the common case of immediately passing the overload set to apply_visitor. The overload set is typically much larger than the variant expression, so passing the variant first tends to be more readable. Makes it look more like switch for types, or a Haskell case expression.

    – Another goal was working with variant-like types other than boost::variant (don’t ask). This turned out to be pretty easy: Just call apply_visitor unqualified from within visit and let ADL pick up the right definition. Custom variants can plug in by defining an apply_visitor in their namespace. One wrinkle here is that inheriting from boost::static_visitor thwarts ADL for non-boost types. Fortunately it turns out that static_visitor isn’t a hard requirement, it’s just a convenience for defining the result_type typedef needed by apply_visitor. You can just as easily typedef Result result_type directly in the overload set, or in a separate static_visitor_adaptor if you want to keep overload_set out of the known-return-type business (relevant to the next item).

    – When you have the variant and the overload set together in one place, you can actually deduce the return type instead of requiring the user to provide it explicitly. The trick is to use a type trait that picks out an arbitrary “argument type” from the variant’s template params (eg, the first one), then pass OverloadSetType(ArgType) to std::result_of. The trait looks something like:


    template
    struct arbitrary_variant_member;

    template
    struct arbitrary_variant_member<boost::variant>
    {
    using type = T;
    };

    // similar specializations for non-boost variants...

    …plus a few more boilerplate specializations to handle const and reference types. I considered trying to deduce an argument type from the overload list instead, but it sounded like more trouble than it was worth, and would rule out or complicate the (admittedly edge) case of visiting with a single generic lambda.

    One drawback to passing the variant and overloads in the same argument list is that you can’t overload visit to handle binary visitation (dispatching on two variants), at least not without passing my TMP pain threshold. But writing a separate visit2 for that case and requiring the caller to be explicit about arity also isn’t much of a burden.

  6. Ugh, of course angle brackets and mangled code. The trait code again, this time with square brackets:

    template [class Variant]
    struct arbitrary_variant_member;
    
    template [class T, class... Ts]
    struct arbitrary_variant_member[boost::variant[T, Ts...]]
    {
        using type = T;
    };
    

    Perhaps some day I’ll remember more than one blogging engine’s comment syntax at a time.

  7. Looks like you spent more time on this than the 15 – 20 minutes that I did.

    I agree that putting the variant first is cleaner looking, I was just trying to stick something in the existing boost::apply_visitor method.

    It’s also probably not too high a price to pay to have to write “binary_visit” in order to support binary visitation if you get to skip the “make_visitor” stuff and pass the handlers directly. I don’t think I’ve ever actually used binary visitation, I just noticed that the make_visitor stuff happened to work and threw that in to the blog post.

    When we’ve switched to VS2015 at work and I can actually use variadic templates (we’re currently on VS2010 and can’t switch until we get the current release out) then I’ll probably do some more work on this and end up with something more along the lines of what you did.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: