Contents
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>...>;
Permalink
Could we define a .visit() templated ,method of std::variant that implements this without requiring thew overload class?
or a global visit function that also takes a list of lambdas without requiring putting the word “overload” around it?
Permalink
Permalink
Is there any way to use std::visit on my own class?
As far as I have seen, it only works on std::variant.
I love the pattern, and would like to use it myself.
But reading through the std::variant code did not lead anywhere…
Permalink
I don’t think that’s possible or advisable.
std::visit
andstd::variant
are tightly knit together, and how the two interact is an implementation detail, so at best, your code wouldn’t be portable.Having said that, I also can’t really imagine a use case where you would want to call
std::visit
on your custom class. Do you have an example where you think this might be beneficial?Permalink
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?
Permalink
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?