Передача лямбды шаблонной функции

195
01 мая 2019, 00:50

Как передать в шаблон с параметром-функцией лямбду? Почему так не работает?

Минимальный пример такой:

template <typename T>
void invoke(std::function<void(T)> f, T val)
{
    f(val);
}
int main()
{
    auto printer = [](int x){ std::cout << x; };
    ::invoke(printer, 42);
}

Я конечно могу явно указать ::invoke<int> или в параметре функции int указать, но тогда это нужно делать при каждом вызове и нет смысла в шаблоне... Как сделать, чтобы в общем случае вызов ::invoke(...) работал?

P.S.: просто поменять std::function на шаблонный параметр нельзя (т.к. нужно переделывать кучу кода тогда)

Answer 1

Если шаблонный параметр фигурирует в типе какого-то параметра функции и при этом находится там в т.наз. дедуцируемом контексте (deduced context), то соответствующий шаблонный аргумент будет дедуцироваться компилятором из типа соответствующего аргумента функции. При этом требуется чтобы:

  1. все дедукции таких шаблонных аргументов (в каждом параметре функции) были успешными, и
  2. все дедукции одного и того же шаблонного аргумента давали один и тот же результат

То есть вот такой пример

template <typename T> struct S { S(T) {} };
template <typename T> void foo(T t, S<T> s) {}
int main() 
{
  foo(5, 5);
}

компилироваться не будет, т.к. дедукция шаблонного аргумента T из типа второго аргумента функции завершается безуспешно. Тот факт, что шаблонный аргумент T успешно дедуцируется из типа первого аргумента функции, никак ситуацию не спасает. В данном примере T находится в дедуцируемом контексте и в первом, и во втором параметре. Это значит, что дедукция должна выполняться через оба параметра и должна завершаться успешно в обоих (и должна выводить одно и то же значение T). В противном случае код некорректен.

Подавить это деструктивное поведение второго параметра функции можно, если в объявлении этого параметра умышленно поместить T в недедуцируемый контекст (non-deduced context). Например, если тип второго параметра описать как вложенный тип постороннего шаблона-оболочки

template <typename T> struct S { S(T) {} };
template <typename T> struct W
{
  using S = ::S<T>;
};
template <typename T> void bar(T t, typename W<T>::S s) {}
int main() 
{
  bar(5, 5);
}

В этом варианте второй аргумент функции исключается из процесса дедукции T и дедукция T выполняется только на основе первого аргумента (и проходит успешно).

Ваш пример страдает от этой же самой проблемы. То есть в вашем примере работоспособный "костыль" может выглядеть так

template <typename T> struct W
{
  using F = std::function<void(T)>;
};
template <typename T> void invoke(typename W<T>::F f, T val)
{
  f(val);
}
int main()
{
  auto printer = [](int x){ std::cout << x; };
  ::invoke(printer, 42);
}

Вполне может быть, что я упускаю уже готовое стандартное средство для решения этой же проблемы этим же способом.

Answer 2

Можно сделать вариадик-шаблон, тогда вам не придется менять уже существующий код:

template <typename function_t, typename... args_t>
decltype(auto) invoke_args(function_t _function, args_t&&... _args)
{
    return (_function)(std::forward<args_t>(_args)...);
}
void f_printer(int x)
{
    std::cout << "f_" << x << std::endl;
}
int main()
{
    auto printer = []() { std::cout << "empty" << std::endl; };
    auto printer1 = [](int x) { std::cout << x << std::endl; };
    auto printer2 = [](int x, int y) { std::cout << x << " " << y << std::endl; };
    invoke_args(f_printer, 42); // f_42
    invoke_args(printer); // empty
    invoke_args(printer1, 42); // 42
    invoke_args(printer2, 42, 84); // 42 84
    system("pause");
}

Такой шаблон будет работать с переменным числом аргументов и будет возвращать тип в соответствии с типом возврата функции

Answer 3

В дополнение к ответу @AnT.

В C++20 в <type_traits> должна появится метафункция std::type_identity, с помощью которой можно будет делать то же самое, что @AnT описывает в ответе:

#include <type_traits>
template <typename T> void invoke(std::type_identity_t<std::function<void(T)>> f, T val)
{
  f(val);
}
int main()
{
  auto printer = [](int x){ std::cout << x; };
  ::invoke(printer, 42);
}

Оригинальное предложение; ревизия оригинального предложения, одобренная в июне 2018.

Answer 4

Как уже указано в комментарии под вопросом, проблема в невозможности вывести тип T из лямбды при инстанцировании шаблона.

Я вижу как минимум два возможных решения:

  • Заменить auto на конкретный тип функтора std::function<void(int)>.
  • Добавить шаблонную версию с двумя параметрами:

    template <typename T, typename F>
    void invoke(F f, T val)
    {
        f(val);
    }

    Тогда лямбда будет распознаваться как тип F в новой реализации.

READ ALSO
Как можно упростить данный код на с++?

Как можно упростить данный код на с++?

Задача: В одномерном целочисленном массиве (размер массива (не больше 20) и значения его элементов вводить с клавиатуры) вычислить сумму элементов...

160
Нужен полный аналог setScaledContents(true), как у QLabel, но только для QGraphicsView

Нужен полный аналог setScaledContents(true), как у QLabel, но только для QGraphicsView

Мне пришлось переписать связку QLabel-QPixmap на связку QGraphicsView-QGraphicsScene-QGraphicsPixmapItem-QPixmapИ вышло как-то совсем не очень

172