C ++ 11 std :: set lambda比较函数

时间:2013-02-15 13:40:37

标签: c++ stl c++11 lambda std-function

我想创建一个带有自定义比较功能的std::set。我可以将它定义为具有operator()的类,但我想享受定义使用它的lambda的能力,所以我决定在类的构造函数的初始化列表中定义lambda函数。 std::set作为会员。但我无法得到lambda的类型。在我继续之前,这是一个例子:

class Foo
{
private:
     std::set<int, /*???*/> numbers;
public:
     Foo () : numbers ([](int x, int y)
                       {
                           return x < y;
                       })
     {
     }
};

我在搜索后找到了两个解决方案:一个,使用std::function。只需设置比较函数类型为std::function<bool (int, int)>并完全像我一样传递lambda。第二种解决方案是编写make_set函数,如std::make_pair

解决方案1:

class Foo
{
private:
     std::set<int, std::function<bool (int, int)> numbers;
public:
     Foo () : numbers ([](int x, int y)
                       {
                           return x < y;
                       })
     {
     }
};

解决方案2:

template <class Key, class Compare>
std::set<Key, Compare> make_set (Compare compare)
{
     return std::set<Key, Compare> (compare);
}

问题是,我是否有充分的理由选择一种解决方案而不是另一种?我更喜欢第一个,因为它使用标准功能(make_set不是标准功能),但我想知道:使用std::function使代码(可能)更慢吗?我的意思是,它是否降低了编译器内联比较函数的可能性,或者它应该足够聪明,行为完全相同,就像它是lambda函数类型而不是std::function(我知道,在这种情况下它不能是lambda类型,但是你知道,我一般都会问)?

(我使用GCC,但我想知道流行的编译器通常做什么)

摘要,我得到了很多好的答案:

如果速度至关重要,最好的解决方案是使用operator() aka仿函数的类。编译器最容易优化并避免任何间接。

为了便于维护和使用C ++ 11功能的更好的通用解决方案,请使用std::function。它仍然很快(比仿函数慢一点,但它可以忽略不计)并且您可以使用任何函数 - std::function,lambda,任何可调用对象。

还有一个使用函数指针的选项,但如果没有速度问题,我认为std::function更好(如果使用C ++ 11)。

有一个选项可以在其他地方定义lambda函数,但是你不会从比较函数中获得lambda表达式,因为你可以使它成为一个operator()的类,并且定义的位置不会'无论如何都是设定的建筑。

还有更多的想法,例如使用委托。如果您想要对所有解决方案进行更全面的解释,请阅读答案:)

6 个答案:

答案 0 :(得分:25)

编译器不太可能内联std :: function调用,而任何支持lambdas的编译器几乎肯定会内联functor版本,包括该functor是否是std::function未隐藏的lambda

您可以使用decltype来获取lambda的比较器类型:

#include <set>
#include <iostream>
#include <iterator>
#include <algorithm>

int main()
{
   auto comp = [](int x, int y){ return x < y; };
   auto set  = std::set<int,decltype(comp)>( comp );

   set.insert(1);
   set.insert(10);
   set.insert(1); // Dupe!
   set.insert(2);

   std::copy( set.begin(), set.end(), std::ostream_iterator<int>(std::cout, "\n") );
}

打印哪些:

1
2
10

看到它在 Coliru 上运行。

答案 1 :(得分:23)

是的,std::function几乎不可避免的间接引入了您的set。理论上,编译器总是可以发现,set的{​​{1}}的所有使用都涉及在一个总是完全相同的lambda的lambda上调用它,这既困难又极其脆弱。

脆弱,因为在编译器可以向自己证明对std::function的所有调用实际上都是对lambda的调用之前,它必须证明对std::function的访问不能设置std::set除了你的lambda之外的任何东西。这意味着它必须跟踪所有可能的路线,以便在所有编译单元中到达std::function,并证明它们都不会这样做。

在某些情况下这可能是可能的,但即使您的编译器设法证明它,相对无害的更改也可能会破坏它。

另一方面,具有无状态std::set的仿函数很容易证明行为,并且涉及到的优化是日常事物。

所以是的,在实践中我怀疑operator()可能会变慢。另一方面,std::function解决方案比std::function解决方案更容易维护,并且为程序性能交换程序员时间是相当可靠的。

make_set有一个严重的缺点,即必须从调用make_set推断出任何此类set的类型。通常make_set存储持久状态,而不是您在堆栈上创建的东西,然后让它超出范围。

如果您创建了静态或全局无状态lambda set,则可以使用auto MyComp = [](A const&, A const&)->bool { ... }语法创建一个可以持久化的std::set<A, decltype(MyComp)>,但编译器可以轻松优化(因为set的所有实例都是无状态仿函数)和内联。我指出了这一点,因为你将decltype(MyComp)放在set中。 (或者你的编译器是否支持

struct

我会感到惊讶!)

最后,如果您担心性能,请考虑struct Foo { auto mySet = make_set<int>([](int l, int r){ return l<r; }); }; 要快得多(代价是无法按顺序迭代内容,并且必须编写/找到好的哈希),并且如果你有一个2阶段的“插入所有内容”然后“重复查询内容”,那么排序的std::unordered_set会更好。只需先将其填入std::vector,然后vector sort unique,然后使用免费的erase算法。

答案 2 :(得分:5)

无状态lambda(即没有捕获的lambda)可以衰减为函数指针,因此您的类型可以是:

std::set<int, bool (*)(int, int)> numbers;

否则我会选择make_set解决方案。如果你不使用单行创建函数,因为它是非标准的,你就不会编写太多的代码!

答案 3 :(得分:1)

根据我使用分析器的经验,性能和美观之间的最佳折衷是使用自定义委托实现,例如:

https://codereview.stackexchange.com/questions/14730/impossibly-fast-delegate-in-c11

由于std::function通常有点太重。我不能评论你的具体情况,但我不知道。

答案 4 :(得分:1)

如果您决定将set作为类成员,在构造函数时初始化其比较器,则至少有一个间接级别是不可避免的。考虑到编译器知道,您可以添加另一个构造函数:

 Foo () : numbers ([](int x, int y)
                   {
                       return x < y;
                   })
 {
 }

 Foo (char) : numbers ([](int x, int y)
                   {
                       return x > y;
                   })
 {
 }

一旦你有了Foo类型的对象,set的类型就不会包含哪个构造函数初始化其比较器的信息,所以要调用正确的lambda需要间接运行-time selected lambda operator()

由于你使用无捕获的lambdas,你可以使用函数指针类型bool (*)(int, int)作为比较器类型,因为无捕获的lambdas具有适当的转换函数。这当然涉及通过函数指针的间接。

答案 5 :(得分:0)

差异在很大程度上取决于编译器的优化。如果它优化了std::function中的lambda,那么它们是等价的,如果没有,你会在前者中引入一个你在后者中不会拥有的间接。