Почему компилятор C++ не может определить тип лямбда-аргумента?

У меня есть следующий код, который не может быть скомпилирован

template <typename T>
void call_with(std::function<void(T)> f, T val) {
    f(val);
}

int main() {
    auto print = [](int x) { std::cout << x; };
    call_with(print, 42);
}

И ошибка компиляции выглядит так

tst.cpp:17:2: error: no matching function for call to 'call_with'
        call_with(print, 42);
        ^~~~~~~~~
tst.cpp:11:6: note: candidate template ignored: could not match 'function<void (type-parameter-0-0)>' against
      '(lambda at tst.cpp:16:15)'
void call_with(std::function<void(T)> f, T val) {
     ^
1 error generated

Я попытался скомпилировать его с помощью g++ tst.cpp -o tst -std=c++17

Итак, я знаю, что иногда C++ может выводить аргументы шаблона при некоторых условиях, но мне интересно, почему в этом случае он не может скомпилировать этот код. Похоже, что не может быть никаких других вариантов, чтобы тип T был чем угодно, кроме int, и чтобы f был чем угодно, кроме std::function<void(int)>.


person Michael Solotky    schedule 29.11.2020    source источник
comment
std::function<void(double)> можно построить из [](int){}. То, что лямбда принимает параметр типа T, не означает, что std::function должен принимать параметр того же типа.   -  person Igor Tandetnik    schedule 29.11.2020
comment
Хорошо, это логично, но если мы оставим только аргумент T val, он будет корректно выведен как int, хотя 42 можно рассматривать как double. Итак, почему существует неоднозначность с std::function и нет неоднозначности с базовым аргументом шаблона T?   -  person Michael Solotky    schedule 29.11.2020
comment
При дальнейшем рассмотрении я считаю, что мой первый комментарий вводит в заблуждение (технически он правильный, но не имеет отношения к вопросу). Настоящая причина в том, что вывод параметров шаблона не учитывает неявные преобразования — он выполняет только простое сопоставление с образцом для типов. И тип class L (какой-то безымянный тип класса, синтезированный для лямбда-выражения) не соответствует std::function<void(T)>   -  person Igor Tandetnik    schedule 29.11.2020
comment
42 нельзя рассматривать как двойник. 42 в качестве выражения представляет собой целое число, которое может соответствовать T. Это бетон. std::function стирает тип вызываемого (любого вызываемого) и делает это путем преобразования. Шаблоны никогда не будут учитывать конверсии при выводе вещей по своему замыслу.   -  person StoryTeller - Unslander Monica    schedule 29.11.2020
comment
Учтите: у std::function<void(T)> есть конструктор template< class F > function( F f ); — на первый взгляд, он может принимать что угодно. У компилятора нет возможности узнать, что он должен искать в аргументе все способы применения к нему оператора вызова функции, а затем сопоставлять с образцом типы параметров такого вызова функции — это часть семантики std::function это никак не отражено в синтаксисе.   -  person Igor Tandetnik    schedule 29.11.2020
comment
В качестве обходного пути вы можете поместить первый T в невыведенный контекст, например. как template <typename T> void call_with(std::function<void(std::type_identity_t<T>)> f, T val) (type_identity_t — это функция C++20, но эквивалент можно тривиально вручную свернуть для более ранних версий). Таким образом, T будет однозначно выводиться из второго аргумента.   -  person Igor Tandetnik    schedule 29.11.2020
comment
Хм, ок, интересно. Таким образом, компилятор может фактически вывести все, как будто для него достаточно информации, но из-за семантики std::function он этого не делает.   -  person Michael Solotky    schedule 29.11.2020
comment
call_with<int>(print, 42); должен решить проблему.   -  person macroland    schedule 29.11.2020
comment
Объяснение Шаблоны не будут учитывать конверсии при выводе вещей, по замыслу, когда-либо. Почему? Потому что время компиляции заняло бы факторно больше времени. И это плохо.   -  person Eljay    schedule 29.11.2020
comment
@Eljay Yeeeah, это похоже на настоящую причину. Хм, а почему факториал?   -  person Michael Solotky    schedule 29.11.2020
comment
Если бы вы могли ограничить все функции монадами, то это было бы только n, но если преобразования для каждого параметра, то у вас есть n x n х ... х n.   -  person Eljay    schedule 29.11.2020


Ответы (1)


Потому что print не является std::function<void(int)>. И его можно преобразовать в более чем один допустимый тип.

Попробуйте запустить

    auto print = [](int x) { std::cout << x; };
    std::cout << typeid(print).name()<<"\n";
    std::function<void(unsigned __int64)> print_a = std::function<void(unsigned __int64)>(print);
    std::cout << typeid(print_a).name() << "\n";
    auto print_b = std::function<void(unsigned __int64)>(print);
    std::cout << typeid(print_b).name() << "\n";

Ты получишь

class <lambda_d103cbf184cf5bdcf1494399ee6d564a>
class std::function<void __cdecl(unsigned __int64)>
class std::function<void __cdecl(unsigned __int64)>

Что показывает, что print является lambda, и его можно преобразовать во многие виды std::function<void(T)>, например:

    auto print = [](int x) { std::cout << x; };
    auto print2 = std::function<void(unsigned __int64)>(print);
    auto print3 = std::function<void(unsigned short)>(print);
    auto print4 = std::function<void(char)>(print);

Можно вывести точный std::function<void(int)> объект:

std::function<void(int)> print = [](int x) { std::cout << x; };
call_with(print, 42);

И 42 также должен быть правильным типом для вывода

std::function<void(int)> + int означает T=int

std::function<void(short)> + short означает T=short

но std::function<void(short)> + int вывести нельзя. Таким образом, вы получите ошибку компиляции:

std::function<void(short)> print = [](short x) { std::cout << x; };
call_with(print, 42);

Правильная версия

std::function<void(short)> print = [](short x) { std::cout << x; };
call_with(print, short(42));
person Kahn    schedule 29.11.2020
comment
Мм, мне это кажется немного странным, как будто есть одно и то же имя типа шаблона T, поэтому, если есть разные кандидаты на function<void>(T), почему компилятор не может просто взять все кандидаты, а затем попытаться вывести второй аргумент функции (который определенно int) и оставить только одного кандидата среди всех, который является function<void>(int) - person Michael Solotky; 29.11.2020