Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cannot initialize std::variant with various lambda expressions

I'm playing with std::variant, lambdas and std::future, and got super weird results when I tried to compose them together. Here are examples:

using variant_t = std::variant<
    std::function<std::future<void>(int)>,
    std::function<void(int)>
>;
auto f1 = [](int) { return std::async([] { return 1; }); };
auto f2 = [](int) { return std::async([] {  }); };

variant_t v1(std::move(f1)); // !!! why DOES this one compile when it SHOULDN'T?
auto idx1 = v1.index(); //equals 1. WHY?

variant_t v2(std::move(f2)); // !!! why DOESN'T this one compile when it SHOULD?

Here is the compilation error:

Error C2665 'std::variant<std::function<std::future<void> (int)>,std::function<void (int)>>::variant': none of the 2 overloads could convert all the argument types

OK, lets change variant's items signatures from returning void to int:

using variant_t = std::variant<
    std::function<std::future<int>(int)>,
    std::function<int(int)>
>;

variant_t v1(std::move(f1)); // COMPILES (like it should)
auto idx1 = v1.index(); // equals 0

variant_t v2(std::move(f2)); // DOESN'T compile (like it should)

What the hell is going on here? Why is std::future<void> so special?

like image 358
Dmitry Katkevich Avatar asked Aug 06 '17 17:08

Dmitry Katkevich


1 Answers

variant's converting constructor template employs overload resolution to determine which type the constructed object should have. In particular, this means that if the conversions to those types are equally good, the constructor doesn't work; in your case, it works iff exactly one of the std::function specializations is constructible from your argument.

So when is function<...> constructible from a given argument? As of C++14, if the argument is callable with the parameter types and yields a type that is convertible to the return type. Note that according to this specification, if the return type is void, anything goes (as any expression can be converted to void with static_cast). If you have a function returning void, the functor you pass in can return anything—that's a feature, not a bug! This is also why function<void(int)> is applicable for f1. On the other hand, future<int> does not convert to future<void>; hence only function<void(int)> is viable, and the variant's index is 1.

However, in the second case, the lambda returns future<void>, which is convertible to both future<void> and void. As mentioned above, this causes both function specializations to be viable, which is why the variant cannot decide which one to construct.

Finally, if you adjust the return type to int, this whole void conversion issue is avoided, so everything works as expected.

like image 177
Columbo Avatar answered Oct 02 '22 13:10

Columbo