Невозможно инициализировать std:: variant с различными лямбда-выражениями

Я играю с std::variant, lambdas и std::future и получаю сверхъестественные результаты, когда я пытался составить их вместе. Вот примеры:

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?

Вот ошибка компиляции:

Ошибка C2665 'std:: variant < std:: function < std:: future <void> (int) > , std:: function < void (int) → :: variant ': ни одна из 2 перегрузок может преобразовывать все типы аргументов

ОК, измените variant подписи элементов от возврата void до 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)

Что, черт возьми, здесь происходит? Почему std::future<void> настолько особенный?

Ответы

Ответ 1

variant шаблон конструктора преобразования использует разрешение перегрузки, чтобы определить, какой тип должен иметь построенный объект. В частности, это означает, что если преобразования с этими типами одинаково хороши, конструктор не работает; в вашем случае это работает, если только одна из спецификаций std::function является конструктивной из вашего аргумента.

Итак, когда function<...> конструктивно из данного аргумента? Начиная с С++ 14, если аргумент вызываем с типами параметров и выводит тип конвертируемый в тип возвращаемого значения. Обратите внимание, что в соответствии с этой спецификацией, если тип возврата void, все идет (как любое выражение может быть преобразовано в void с помощью static_cast). Если у вас есть function, возвращающий void, функтор, который вы передаете, может вернуть что-нибудь, а не какую-то ошибку! Именно поэтому function<void(int)> применим для f1. С другой стороны, future<int> не преобразуется в future<void>; следовательно, только function<void(int)> является жизнеспособным, а индекс варианта равен 1.

Однако во втором случае lambda возвращает future<void>, который можно конвертировать в оба future<void> и void. Как упоминалось выше, это приводит к тому, что специализации function являются жизнеспособными, поэтому variant не может решить, какой из них следует построить.

Наконец, если вы отредактируете возвращаемый тип на int, вся эта проблема преобразования void будет устранена, поэтому все будет работать так, как ожидалось.