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


75

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

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

搜索后,我发现了两种解决方案:一种是使用std::function。只需将设置比较函数类型设置为be,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()函子的类。编译器最容易优化和避免任何间接调用。

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

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

有一个选项可以在其他地方定义lambda函数,但是随后您就无法从作为lambda表达式的比较函数中获得任何收益,因为您也可以将其作为一个类,operator()并且定义的位置也不会成为set构造。

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


2
我闻到过早的优化。

为什么不只是bool(*)(int, int)呢?但是,创建一个显式的,可默认构造的谓词类可能会更有效。
Kerrek SB

@Fanael您将如何知道,如果我有一组由GUI渲染的对象,而我真的需要尽快地做到这一点
cfa45ca55111016ee9269f0a52e771 2013年

@ fr33domlover:std::function在这种情况下,渲染成本是否不算小巫见大巫?

@Fanael如果在没有渲染的情况下完成了排序,则可以通过加快排序速度并为渲染代码提供更多执行时间来提高速度。无论如何,即使这是过早的优化,问题仍然有用:看看答案并投票...
cfa45ca55111016ee9269f0a52e771 2013年

Answers:


26

是的,std::function几乎为您带来了不可避免的间接访问set。从理论上讲,尽管编译器总能弄清楚您set对的所有使用都std::function涉及在总是完全相同的lambda的lambda上调用它,这既困难又极其脆弱。

易碎,因为在编译器可以向自己证明std::function对它的所有调用实际上都是对您的lambda的调用之前,它必须证明对您的访问std::set永远不会将除std::functionlambda之外的任何值设置为。这意味着它必须跟踪所有可能的途径,以到达std::set所有编译单元中的路径,并证明它们中没有一个这样做。

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

另一方面,具有无状态的函子operator()容易证明行为,而涉及到的优化是日常事务。

所以,是的,实际上我怀疑std::function会慢一些。另一方面,std::function解决方案比make_set一种解决方案更易于维护,并且交换程序员时间以提高程序性能是很容易实现的。

make_set具有严重的缺点,set必须从对的调用中推断任何这样的类型make_set。通常是set存储持久状态,而不是在堆栈上创建的持久状态,然后超出范围。

如果创建了静态或全局无状态lambda 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; });
};

我会感到惊讶!)

最后,如果您担心性能,请考虑这样std::unordered_set做会更快(以无法依次遍历内容为代价,并且必须编写/查找良好的哈希值),并且std::vector如果有两阶段“插入所有内容”,然后“重复查询内容”。只需将其填充到第vector一个中,然后填充sort unique erase,然后使用免费equal_range算法即可。


关于备用容器的好的建议,但请注意其他一些类型的哈希函数(int可以正常使用)。如果操作不当,可能会降低性能-速度太慢或发生太多碰撞。
金属版

我想我用make_set错过了这一点,但不适用于lambda。剩下的只是std :: function解决方案,它确实涉及间接性,但是目前我还没有性能问题。矢量也非常有趣,设置的内容由GUI读取并渲染,因此渲染发生的频率更高,用户更改了内容(这种情况很少发生)...也许在每次更改时对矢量进行排序实际上比键查找要快
cfa45ca55111016ee9269f0a52e771 2013年

auto functor = [](...){...};语法具有比短的优点struct functor { bool operator()(...)const{...}; };的语法,以及要求的缺点functor,以调用它的实例(相对于任何默认构造算符为struct情况)。
Yakk-Adam Nevraumont

31

编译器不太可能能够内联std :: function调用,而任何支持lambda的编译器几乎都可以内联functor版本,包括该functor是不被a隐藏的lambda std::function

您可以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


您只能在定义lambda之后使用decltype,但是随后我失去了在构造函数本身中定义lambda的能力,因此我也可以将class与operator()一起使用。我想在构造函数中定义比较函数
cfa45ca55111016ee9269f0a52e771

往上看。您可以将其设为班级的静态成员。
金属版

编辑评论(代码示例):看一下代码示例,这是不同的。您的建议不起作用,因为无法在未评估的上下文中使用lambda,并且无法正确推断类型,因为它是为您创建的每个单独的lambda编译器生成的。我也找到了相关的SO问题,他们说的很准确。否则,我只会使用lambda ...
cfa45ca55111016ee9269f0a52e771

对。只是测试而已。删除了那个位。
金属版

1
@ cfa45ca55111016ee9269f0a52e771 lambda类型仅取决于返回类型和参数类型。您可以使用具有相同返回值和参数的任何lambda。您可以decltype在其上使用一个小的lambda示例:decltype([](bool A,bool B){r​​eturn bool(1)})`。
Euri Pinhollow

6

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

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

否则,我会寻求make_set解决方案。如果您不使用单行创建功能(因为它是非标准的),则不会编写太多代码!


有趣的是,我不知道它会转换为函数指针...至于标准/非标准,我的意思是,如果我依靠std :: function在将来更好地实现,那么将来的编译器版本将使其成为现实与lambda一样快,内联,无需更改代码
cfa45ca55111016ee9269f0a52e771 2013年

1
我懂了。std::function获得效率有一个限制,但是在担心此性能之前,您是否实际进行过测量以查看是否存在性能问题?std:function可能非常快:timj.testbit.eu/2013/01/25/cpp11-signal-system-performance
Jonathan Wakely

1
@ fr33domlover:您的假设不一定正确。定义的定义std::function要求对所持有的实际可调用实体进行类型擦除,而这几乎需要一个间接调用。即使在使用普通函数指针的情况下,也要比为该特定目的创建函子而花费更多的钱。这正是为什么std::sort它比C更快的原因qsort
大卫·罗德里格斯(DavidRodríguez)-dribeas

1

根据我在探查器中玩耍的经验,性能和美观之间的最佳折衷方案是使用自定义委托实现,例如:

/codereview/14730/impossfully-fast-delegate-in-c11

由于std::function通常太重了。我无法评论您的具体情况,但我不知道。


看起来是一个出色的通用解决方案,但我只需要把std :: set设置为小写即可,我更喜欢只使用make_set,它是通用委托类的“特殊情况”。但总的来说,这很有趣,也许可以解决所有这些lambda问题
cfa45ca55111016ee9269f0a52e771 2013年

2
这样的代理在无状态函子上仍然具有不透明的间接层(即,相当于指针取消引用)。使用无状态函子的能力是其std::sort表现出色的主要原因之一qsort
Yakk-Adam Nevraumont

哦,如果它具有非内联间接,我也可以使用std :: function并获得相同的速度...
cfa45ca55111016ee9269f0a52e771 2013年

1
我可能没有能力gdb,但是在我看来,当我在-O3中使用该委托时,编译器通常会消除所有取消引用。
user1095108 2013年

1

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

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

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

一旦有了类型的对象Foo,的类型set就不包含构造函数初始化其比较器的信息,因此要调用正确的lambda,需要对运行时选定的lambda进行间接调用operator()

由于您正在使用不捕获的lambda,因此可以将函数指针类型bool (*)(int, int)用作比较器类型,因为不捕获的lambda具有适当的转换功能。当然,这将涉及通过函数指针的间接访问。


0

差异很大程度上取决于编译器的优化。如果它在std::function与lambda相同的情况下优化了lambda ,那么如果不优化,则您会在前者中引入一个间接值,而后者则不会。


我使用GCC,但是我想知道流行的编译器通常会做什么。可能的间接原因是我不直接选择std :: function解决方案的原因
cfa45ca55111016ee9269f0a52e771 2013年


嗯...它说编译器仍然没有完全处理std :: function的简单情况
cfa45ca55111016ee9269f0a52e771 2013年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.