Cannot initialize std::variant with various lambda expressions
Asked Answered
D

1

15

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?

Diner answered 6/8, 2017 at 17:17 Comment(2)
Note: std::variant is a C++17 feature, not C++11.Pup
Your compile error is from the second example, but your question makes it seem like it's from your first.Lisp
S
16

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.

Schreibman answered 6/8, 2017 at 17:43 Comment(2)
thanks for your reply. But I still cannot imagine how std::future<int> can be implicitly convertible to void (or void to std::future<int> - i'm not sure i understood this point)? Does it mean what everithing is convertible to void?Diner
@DmitryKatkevich That is correct, any expression can be converted to void via static_cast. Otherwise, we could not supply functors to function that return something when the function yields void, which is a desired feature!Schreibman

© 2022 - 2024 — McMap. All rights reserved.