无法从Lambda函数中推断出std::function的模板参数

3

在探索C++模板时,我偶然发现了以下代码中的示例:

#include <iostream>
#include <functional>

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

int main(int argc, char const *argv[])
{
    auto foo = [](int i) {
        std::cout << i << std::endl;
    };
    call(foo, 1);
    return 0;
}

为了编译这个程序,我正在使用GNU C++编译器g++:
$ g++ --version // g++ (Ubuntu 6.5.0-1ubuntu1~16.04) 6.5.0 20181026

在编译为C++11后,我遇到了以下错误:

$ g++ -std=c++11 template_example_1.cpp -Wall

template_example_1.cpp: In functionint main(int, const char**)’:
template_example_1.cpp:15:16: error: no matching function for call tocall(main(int, const char**)::<lambda(int)>&, int)’
     call(foo, 1);
                ^
template_example_1.cpp:5:6: note: candidate: template<class T> void call(std::function<void(T)>, T)
 void call(std::function<void(T)> f, T v)
      ^~~~
template_example_1.cpp:5:6: note:   template argument deduction/substitution failed:
template_example_1.cpp:15:16: note:   ‘main(int, const char**)::<lambda(int)>’ is not derived from ‘std::function<void(T)>’
     call(foo, 1);
                ^

C++14C++17同样适用)

根据编译器错误和注释,我了解到编译器无法推断lambda的类型,因为它不能与std :: function匹配。

查看之前的问题(1234),我仍然对此感到困惑。

正如第3和4个问题的答案所指出的那样,可以通过显式指定模板参数来修复此错误,例如:

int main(int argc, char const *argv[])
{
    ...
    call<int>(foo, 1); // <-- specify template argument type
    // call<double>(foo, 1) // <-- works! Why?
    return 0;
}

然而,当我使用除 int 之外的其他类型,如 doublefloatcharbool 时,它同样有效,这让我更加困惑。

所以,我的问题如下:

  • 为什么在显式指定 int(和其他类型)作为模板参数时它能够工作?
  • 是否有更通用的方法来解决这个问题?

为什么要使用 std::function?为什么不能只用 T f - tkausl
使用@tkausl和T f,我将无法将f用作函数。 - omar
@omar tkausl 可能指的是 F f - Yakk - Adam Nevraumont
1个回答

12

std::function不是一个lambda表达式,而lambda表达式也不是std::function

Lambda表达式是一个带有operator()和其他一些辅助功能的匿名类型。您的:

auto foo = [](int i) {
    std::cout << i << std::endl;
};

是...的简写

struct __anonymous__type__you__cannot__name__ {
  void operator()(int i) { 
    std::cout << i << std::endl;
  }
};
__anonymous__type__you__cannot__name__ foo;

大致地说(实际上还有转换为函数指针和其他一些噪音我不会涉及)。

但是,请注意它并没有继承自std::function<void(int)>


一个lambda函数无法推导出std::function的模板参数,因为它们是不相关的类型。模板类型推导是根据传递的参数类型及其基类进行精确的模式匹配,而不会尝试使用任何类型的转换。

std::function<R(Args...)> 是一种类型,可以存储任何可复制的内容,该内容可以使用与 Args... 兼容的值进行调用,并返回与 R 兼容的内容。

因此,std::function<void(char)> 可以存储能够使用 char 进行调用的 任何 内容。由于可以使用 char 调用 int 函数,因此这也适用。

试一试:

void some_func( int x ) {
  std::cout << x << "\n";
}
int main() {
  some_func('a');
  some_func(3.14);
}

std::function可以将其签名转换为其内部存储的可调用对象。


最简单的解决方案是:

template <class F, class T>
void call(F f, T v) {
  f(v);
}

现在,极少数情况下,您实际上需要签名。 您可以在中执行此操作:

template<class T>
void call(std::function<void(T)> f, T v) {
  f(v);
}
template<class F, class T>
void call(F f_in, T v) {
  std::function f = std::forward<F>(f_in);
  call(std::move(f), std::forward<T>(v));
}

最后,你的call是一个残缺版本的中的std::invoke。考虑使用它;如果不想使用,则使用回溯版本。


是的,那应该可以了。我也正要写同样的东西。 - Kagiso Marvin Molekwa
此外,显式转换有效是因为它不再是模板,所以隐式转换可以启动。 - Quimby
如果你要使用 std::forward,那么你应该接受一个 F&&T&& - tkausl
如果有人显式地传递模板参数,则使用std::move是错误的。无论如何,使用std::forward都是正确的。我考虑过使用std::move,但除了常规惯例保证“F”和“T”不是引用之外,没有其他东西可以支持该决定,所以我选择正确性而非简单性。 - Yakk - Adam Nevraumont
@Yakk-AdamNevraumont 感谢您详尽的回答。我想将一个 lambda 表达式传递给我的 cal() 函数,由于 lambda 是一个 callable,因此我认为最好的方法是将其包装在 std::function 中(std::function 的实例可以存储、复制和调用任何 Callable 目标)。 - omar
@omar std::function 是一种类型擦除类。类型擦除和模板类型推导是彼此的伪逆。同时使用两者是代码异味,通常是设计错误的标志。这就像有一个函数接受一个 std::mutex 并对其进行解锁和重新锁定。或者一个函数将一个 int 值加倍,再将其减半,然后返回该值。同时执行这两个操作几乎等同于什么都不做,除了在某些情况下通常会出现错误。极少情况下,这样做可能是有意为之并且是一个好计划。 - Yakk - Adam Nevraumont

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接