Overload: Build a Variant Visitor on the Fly

Having written about std::variant and std::visit last week, it’s time to string together some modern C++ features to build a naive basic implementation of overload, a proposed C++ feature.

Recap: visitor requirements

As described in the last post, std::visit needs a function object that has overloads that accept all possible variant alternatives. It’s OK if the overloads do not exactly match since the compiler will use normal overload resolution:

void bar() {
  std::variant<double, bool, std::string> var;

  struct {
    void operator()(int) { std::cout << "int!\n"; }
    void operator()(std::string const&) { std::cout << "string!\n"; }
  } visitor;

  std::visit(visitor, var);
}

The above example will print int! if the variant holds a double or a bool because both are convertible to int. Whether or not that is what we want is another thing – but that’s what unit tests are for 😉

Can I have that on the fly?

In the above example, we defined an anonymous struct directly before we used it with std::visit. In the struct, we simply overload operator() for the types we need. This is close, but not immediately inside the call as we by now are used to when we use lambdas.

Lambda and constexpr if

In fact, we could use a generic lambda, and in many cases, it can do what we want:

void foo() {
  std::variant<int, std::string> var;
  std::visit(
    [](auto&& value) {
      if constexpr(std::is_same_v<decltype(value), std::string&>) {
        std::cout << "a string of length " << value.length() << '\n';
      } else {
        std::cout << "an int\n";
      }
    },
    var
  );
}

In this example, we use C++17’s if constexpr and the type trait std::is_same to have one branch for every variant alternative. This approach has some drawbacks though:

  • It does not perform overload resolution, so in our first example, where we have a bool or double in the lambda, std::is_same_v<decltype(value), int const&> would return false.
  • We have to take care of const, volatile and reference specifiers, either by knowing exactly what propagating the variant content to the lambda will give us, or by using std::decay_t<decltype(value)>.
  • It gets verbose and ugly really quick

Let’s overload lambdas!

Overloading operator() in a struct seems to be the better alternative in many cases, but we want the convenience of lambdas. Sadly, a lambda only has one single operator() that can not be overloaded. So, how do we get the best of both worlds? The answer is to build a struct that consists of several lambdas and has all their operator()s.

To be able to use those operators, it is easiest to inherit from the lambdas and import the operators with a using declaration. Let’s do that for our original struct in the first example above:

template <class F1, class F2>
struct overload2 : F1, F2 {
  overload2(F1 const& f1, F2 const& f2) : F1{f1}, F2{f2} 
  {}

  using F1::operator();
  using F2::operator();
};

In theory, this will work. We only need some way to instantiate the template. Stating the types of F1 and F2 is impossible though since we are dealing with lambdas that do not have a pronounceable type. Luckily we have class template argument deduction in C++17, and the automatic deduction guides will just work:

int main() {
  std::variant<std::string, int> var;
  std::visit(
    overload2(
      [](int){std::cout << "int!\n";}, 
      [](std::string const&){std::cout << "string!\n";}
    ),
    var
  );
}

Nice. The next thing is to make the overload work for any number of lambdas. With variadic templates, it’s rather straightforward:

template <class ...Fs>
struct overload : Fs... {
  overload(Fs const&... fs) : Fs{fs}...
  {}

  using Fs::operator()...;
};

Yes, that’s right. The three dots in basically every line. One thing to note is that the ellipsis in the using declaration is only allowed in C++17 and later.

With this tiny snippet, we can go crazy on the lambda overloads. We even can have a generic lambda, which basically will be the “default” case:

int i;
//...
std::visit(
  overload(
    [i](int j) { std::cout << "it's an int, and it is " << ((j==i) ? "equal" : "not equal") << " to 'i'\n"; },
    [](std::string) { std::cout << "it's a string\n"; },
    [](auto&&) { std::cout << "it's something else!\n"; }
  ),
  var;
);

Note, however, that in overload resolution, the generic lambda will be the better match than any type conversion. In this last example, if the lambda contains a bool or a double, it would not be converted to int.

A few generalizations

Currently, the snippet above copies the lambdas into the base objects. What if we have lambdas that cannot be copied but only moved? And what about other function objects that are already constructed? The answer is to use forwarding references and make the constructor a template:

template <class ...Fs>
struct overload : Fs... {
  template <class ...Ts>
  overload(Ts&& ...ts) : Fs{std::forward<Ts>(ts)}...
  {} 

  using Fs::operator()...;
};

Now, since the constructor is a template, automatic deduction guides do not work anymore, so we have to provide one, taking care of references:

template <class ...Ts>
overload(Ts&&...) -> overload<std::remove_reference_t<Ts>...>;

Nowe we can use a lot more function objects, move-only lambdas and so on:

struct X{};

int main() {
  auto f1 = std::function<void(int)>{ [](int){ std::cout << "int!\n";} };
  auto ux = std::make_unique<X>();

  std::variant<std::string, int> var;
  std::visit(
    overload(
      f1, //copied
      [ux = std::move(ux)](std::string const&){ std::cout << "std::string!\n"; } //move only
    ),
    var
  );
}

The std::overload proposal

Of course, there’s still more work to do: This does not work with function pointers, as we can not derive from those. It also does not work with function objects that are of a final type, i.e. can not be derived from. Luckily, especially the latter is a rare corner case that only library writers have to worry about.

All of this and more is taken care of in a proposal for the standard library: P0051

Until this is in the standard, we can enjoy these few lines packed with a combination of modern C++ features that go hand in hand.

template <class ...Fs>
struct overload : Fs... {
  template <class ...Ts>
  overload(Ts&& ...ts) : Fs{std::forward<Ts>(ts)}...
  {} 

  using Fs::operator()...;
};

template <class ...Ts>
overload(Ts&&...) -> overload<std::remove_reference_t<Ts>...>;
Previous Post
Next Post

2 Comments


  1. Hi,

    You generalization code doesn’t work on gcc 8.1. I get mismached pack lengths while expandings Fs (https://godbolt.org/g/87tLVG). The version without the templated constructor works fine.
    On clang you get problems with variant, but the experimental concepts version works. It bizarre that we have to wait for a next version of clang to have variant work, but that’s another issue.
    Any ideas? What part of gcc 8.1 that is not c++17 compliant, which prevents this from working?

    Reply

    1. For GCC, that is a known bug. I ran into it when I compiled it on Godbolt. I can not find the link to the bug report again, but one of the code samples to reproduce the code looks pretty much like overload (Thanks, Vittorio…)

      It works on some version of clang – I used wandbox to compile the code in this post: https://wandbox.org/permlink/u8AQzk00RTgbarBo I don’t know the difference between that one and the Godbolt Clang though – maybe a different standard library implementation?

      Reply

Leave a Reply

Your email address will not be published. Required fields are marked *