将 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::function 只是对函数参数的一个小注释——你会发现std::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&lt;double&gt;(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 predicatePredicate&amp;&amp; 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&lt;bool(T)&gt; 时可选。

您可以在呼叫站点修复它。

您可以手动传递T binsearch&lt;T&gt;(vec, [](int x)return x&lt;0;)

您可以将 lambda 衰减为函数指针,方法是在其前面放置一个 + +[](int x) ... 或 static_cast&lt;bool(*)(int)&gt;( ... )

最好的选择是Predicate 一个。这也是标准库代码的作用。

我们还可以更进一步,让您的代码更加通用:

template <class Range, class Predicate>
auto binsearch(const Range &ts, Predicate&& predicate)
-> typename std::decay< decltype(*std::begin(ts)) >::type

-&gt; typename std::decay ... 尾随返回类型部分可以在 C++14 中消除。

这样做的好处是,如果主体也使用std::beginstd::end 来查找开始/结束迭代器,binsearch 现在支持deques、平面C 样式数组、std::arrays、@ 987654342@s、std::vectors,甚至还有一些自定义类型。

【讨论】:

【参考方案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&lt;bool(T const&amp;)&gt; 中。为此,只需将上面的函数重写为:

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&lt;bool(int)&gt;。您的签名将如下所示:

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 作为模板函数参数传递

为啥将 lambda 传递给受约束的类型模板参数会导致“不完整类型”编译器错误?

C ++ lambda作为函数的模板参数不起作用

将 lambda 参数完美转发给成员函数,其中成员函数是非类型模板形参

将模板函数转换为通用 lambda

为啥我不能将模板函数指针传递给可变参数函数?