将 lambda 传递给函数模板
Posted
技术标签:
【中文标题】将 lambda 传递给函数模板【英文标题】:Passing a lambda into a function template 【发布时间】:2017-03-07 05:06:18 【问题描述】:我正在学习 C++,我正在尝试实现一个二进制搜索函数,该函数可以找到谓词所包含的第一个元素。该函数的第一个参数是一个向量,第二个参数是一个计算给定元素的谓词的函数。二分查找函数如下所示:
template <typename T> int binsearch(const std::vector<T> &ts, bool (*predicate)(T))
...
如果像这样使用,这将按预期工作:
bool gte(int x)
return x >= 5;
int main(int argc, char** argv)
std::vector<int> a = 1, 2, 3;
binsearch(a, gte);
return 0;
但是如果我使用 lambda 函数作为谓词,我会得到一个编译器错误:
search-for-a-range.cpp:20:5: error: no matching function for call to 'binsearch'
binsearch(a, [](int e) -> bool return e >= 5; );
^~~~~~~~~
search-for-a-range.cpp:6:27: note: candidate template ignored: could not match 'bool (*)(T)' against '(lambda at
search-for-a-range.cpp:20:18)'
template <typename T> int binsearch(const std::vector<T> &ts,
^
1 error generated.
以上错误是由
产生的binsearch(a, [](int e) -> bool return e >= 5; );
怎么了?为什么编译器不相信我的 lambda 具有正确的类型?
【问题讨论】:
将 bool (*predicate)(T) 更改为 std::functionstd::lower_bound
(你正在重新实现)将一对迭代器而不是一个容器作为它的参数.这允许它与任何类型的容器、容器的子集、甚至是标准库设计者尚未想到的范围一起工作。当您的代码在std::vector
上运行时,我强烈建议您以这种方式使其更通用;我保证你会学到一些东西!
@TobySpeight 他需要的不仅仅是一个谓词。谓词必须是顺序关系,并且他需要一个目标值
呃,我的意思是std::find_if()
,而不是std::lower_bound()
。两者都具有指导意义,实施自己的方法是很好的练习,所以其余的仍然有效。
@Evgeniy 绝对不会。这已经是一个模板,在这里类型擦除没有任何好处。
【参考方案1】:
您的函数binsearch
将函数指针作为参数。 lambda 和函数指针是不同的类型:lambda 可以被视为实现 operator()
的结构的实例。
请注意,无状态 lambda(不捕获任何变量的 lambda)可以隐式转换为函数指针。由于模板替换,这里的隐式转换不起作用:
#include <iostream>
template <typename T>
void call_predicate(const T& v, void (*predicate)(T))
std::cout << "template" << std::endl;
predicate(v);
void call_predicate(const int& v, void (*predicate)(int))
std::cout << "overload" << std::endl;
predicate(v);
void foo(double v)
std::cout << v << std::endl;
int main()
// compiles and calls template function
call_predicate(42.0, foo);
// compiles and calls overload with implicit conversion
call_predicate(42, [](int v)std::cout << v << std::endl;);
// doesn't compile because template substitution fails
//call_predicate(42.0, [](double v)std::cout << v << std::endl;);
// compiles and calls template function through explicit instantiation
call_predicate<double>(42.0, [](double v)std::cout << v << std::endl;);
您应该使您的函数binsearch
更通用,例如:
template <typename T, typename Predicate>
T binsearch(const std::vector<T> &ts, Predicate p)
// usage
for(auto& t : ts)
if(p(t)) return t;
// default value if p always returned false
return T;
从standard algorithms library获取灵感。
【讨论】:
谢谢!我认为这比我预期的要严格一些。例如,您的方法将允许传递一个整数向量和一个将 char(不是 int)映射到 bool 的谓词。不过,对于我的用例来说可能并不重要。 如果您正在执行诸如将 int 传递给采用 char 的谓词之类的事情,您的编译器应该会抱怨精度损失。如果不是,请更改您的编译器;) 无状态 lambdas 将转换为函数指针。如果不是模板参数 T,问题中的示例将编译。在这方面,答案具有误导性,恕我直言。 @ComicSansMS 我编辑了我的答案。它的误导性是否更小? 我可以建议添加第四个示例:call_predicate<double>(42.0, ...
。不是模板不起作用,是模板参数推导在这里失败了。【参考方案2】:
带有空捕获列表的lambda expression 可以隐式转换为函数指针。但是函数指针predicate
是以T
为参数,需要推导。模板类型推导不考虑类型转换,不能推导T
;正如错误消息所说,候选模板(即binsearch
)被忽略。
你可以使用operator+
来实现这个,它将lambda转换为函数指针,稍后将传递给binsearch
,然后T
将被成功推导[1] .
binsearch(a, +[](int e) -> bool return e >= 5; );
// ~
当然你可以明确地使用static_cast
:
binsearch(a, static_cast<bool(*)(int)>([](int e) -> bool return e >= 5; ));
请注意,如果您将predicate
的类型更改为独立于T
,即bool (*predicate)(int)
,则传递带有空捕获列表的lambda 也可以; lambda 表达式将被隐式转换为函数指针。
另一种解决方案是将参数类型从函数指针更改为std::function
,这对于函子来说更通用:
template <typename T> int binsearch(const std::vector<T> &ts, std::function<bool (typename std::vector<T>::value_type)> predicate)
...
然后
binsearch(a, [](int e) -> bool return e >= 5; );
[1] A positive lambda: '+[]' - What sorcery is this?
【讨论】:
@david 静态转换如何更具可读性?还有更多内容要阅读,但所有内容都在重复旁边所说的内容。可靠地起作用的魔法不需要把自己拼出来。 阅读更多!= 阅读更少。只有 C++ 大师才能理解的神奇功能(例如 + 和 lambda 函数)== 不可读。 我真的很喜欢“+”魔法,但我刚刚意识到如果 lambdas 捕获局部变量,它们就不能转换为函数指针。尽管如此,我还是从你的回答中学到了很多。 @DavidHaim Lambda 不是“只有 C++ 大师才能理解的神奇功能”。在这一点上,任何使用 C++ 编程的人都应该熟悉 lambda。+
衰减 lambda 无疑是神奇的,但如果你不知道它的作用,那也没关系。如果你看一下这条线,它实际上做了它看起来做的事情如果你不了解魔法。它是如何做到的很神奇,但是非大师不需要知道魔法来理解这条线。你不需要知道+
是如何工作的,就像你需要理解向量化来编写一个快速的for()
循环一样。
Lambdas 确实是基本的,没有争议。使用+
作为 lambda 到函数的转换器非常具体且不常见。我认为没有理由使用大多数 C++ 开发人员并不真正了解的东西,而是非常知名和常见的 static_cast
。但我想我不会说服你【参考方案3】:
为什么编译器不相信我的 lambda 具有正确的类型?
模板函数被告知推断其模板参数不进行转换。 lambda 不是函数指针,因此无法推断出该参数中的 T
。由于所有函数参数都独立推导它们的模板参数(除非推导被阻止),这会导致错误。
您可以进行许多修复。
您可以修复模板功能。
template <class T>
int binsearch(const std::vector<T> &ts, bool (*predicate)(T))
将函数指针替换为Predicate predicate
或Predicate&& predicate
并保持主体不变。
template <class T, class Predicate>
int binsearch(const std::vector<T> &ts, Predicate&& predicate)
使用扣分阻塞:
template<class T>struct tag_tusing type=T;;
template<class T>using block_deduction=typename tag_t<T>::type;
template <class T>
int binsearch(const std::vector<T> &ts, block_deduction<bool (*)(T)> predicate)
在将函数指针替换为std::function<bool(T)>
时可选。
您可以在呼叫站点修复它。
您可以手动传递T
binsearch<T>(vec, [](int x)return x<0;)
。
您可以将 lambda 衰减为函数指针,方法是在其前面放置一个 +
+[](int x)
... 或 static_cast<bool(*)(int)>(
... )
。
最好的选择是Predicate
一个。这也是标准库代码的作用。
我们还可以更进一步,让您的代码更加通用:
template <class Range, class Predicate>
auto binsearch(const Range &ts, Predicate&& predicate)
-> typename std::decay< decltype(*std::begin(ts)) >::type
-> typename std::decay
... 尾随返回类型部分可以在 C++14 中消除。
这样做的好处是,如果主体也使用std::begin
和std::end
来查找开始/结束迭代器,binsearch
现在支持deque
s、平面C 样式数组、std::array
s、@ 987654342@s、std::vector
s,甚至还有一些自定义类型。
【讨论】:
【参考方案4】:如果你对binsearch
有任何控制权,我建议你重构它:
template <typename T, typename Predicate>
int binsearch(std::vector<T> const& vec, Predicate&& pred)
// This is just to illustrate how to call pred
for (auto const& el : vec)
if (pred(el))
// Do something
return 0; // Or anything meaningful
另一种方法是对仿函数对象/函数指针/任何东西执行类型擦除...通过将它们嵌入到std::function<bool(T const&)>
中。为此,只需将上面的函数重写为:
template <typename T>
int binsearch(std::vector<T> const& vec, std::function<bool(T const&)> pred);
但是由于模板参数推导不做任何转换,你需要像下面这样显式地提供你的函数:
auto my_predicate = [](int x) return true; ; // Replace with actual predicate
std::vector<int> my_vector = 1, 2, 3, 4;
binsearch(my_vector, std::function<bool (int const&)>(my_predicate));
但是鉴于您的功能描述,它似乎与std::find_if
完成相同的工作。
std::vector<int> my_vector = 1, 12, 15, 13, 16;
auto it = std::find_if(std::begin(my_vector), std::end(my_vector),
[](int vec_el) return !vec_el%5; );
// it holds the first element in my_vector that is a multiple of 5.
if (it != std::end(my_vector))
std::cout << *it << std::endl; // prints 15 in this case
请注意,要进行二分搜索,您需要的不仅仅是一个谓词:您需要一个谓词来定义范围内的顺序和目标值。
【讨论】:
调用谓词时不要重复转发。第一次转发时,您承诺不再调用它。删除前锋以解决此问题;检测最后一次迭代并有条件地转发(虽然正确)将使代码变得丑陋,产量很少。 切换到std::function
不会解决这里的问题。
@ComicSansMS 对不起,这句话有误导性...为了清楚起见,编辑了它
@Rerito 对不起,但我认为它仍然具有误导性。切换到std::function
允许我传递有状态的lambda(就像第一个使用Predicate
模板参数的解决方案一样)。但它根本没有解决问题中的问题:如果我将T
埋在参数类型中足够深,那么模板参数推导失败的事实。函数指针和std::function
方法都会发生这种情况。
@ComicSansMS 我不认为我完全明白你的意思。你能提供一个我自己的指令的例子吗?【参考方案5】:
函数指针和 lambda 函数不是一回事。
无法将对象t
分配给谓词 where:
bool (*predicate)(int)
和
auto t = [](int e) -> bool return e >= 5; );
不妨使用std::function<bool(int)>
。您的签名将如下所示:
template <typename T>
int binsearch(const std::vector<T> &ts, std::function<bool(T)> predicate)
// ...
现在这不是函数指针,如果有必要,你需要绑定你的函数指针,(我假设你只用 lambdas 就可以了)
【讨论】:
这可能会产生不可取的开销。请注意,标准库不会这样做,因为没有必要。您可以改为为谓词添加模板参数。 当然,您可以像这样将无状态 lambda 分配给函数指针。 Proof on coliru。破坏它的是未指定的模板参数。切换到std::function
也不能解决这里的问题,只要不指定T
即可。以上是关于将 lambda 传递给函数模板的主要内容,如果未能解决你的问题,请参考以下文章
为啥将 lambda 传递给受约束的类型模板参数会导致“不完整类型”编译器错误?