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

Posted

技术标签:

【中文标题】C++11 std::set lambda 比较函数【英文标题】:C++11 std::set lambda comparison function 【发布时间】:2013-01-31 12:09:31 【问题描述】:

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

class Foo

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

我搜索后发现了两种解决方案:一种,使用std::function。只需将设置比较函数类型设为std::function&lt;bool (int, int)&gt; 并像我一样传递 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 functor 的类。编译器最容易优化和避免任何间接。

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

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

有一个选项可以在其他地方定义 lambda 函数,但是如果比较函数是 lambda 表达式,您将一无所获,因为您也可以使用 operator() 将其设为一个类,并且定义的位置不会是无论如何设置构造。

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

【问题讨论】:

我闻到过早的优化。 为什么不只是bool(*)(int, int)?但是创建一个显式的、可默认构造的谓词类可能更有效。 @Fanael 你怎么知道,如果我有一长串由 GUI 渲染的对象,我真的需要它尽可能快 @fr33domlover:在这种情况下,std::function 的成本与渲染成本相比是不是相形见绌? @Fanael 如果在没有渲染的情况下完成排序,我仍然可以通过加快排序并为渲染代码提供更多执行时间来获得更快的速度。无论如何,即使如果这是过早的优化,问题仍然有用:查看答案并点赞... 【参考方案1】:

编译器不太可能内联 std::function 调用,而任何支持 lambda 的编译器几乎肯定会内联仿函数版本,包括如果该仿函数是未被 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 上的实时运行。

【讨论】:

你只能在定义了 lambda 之后使用 decltype,但是我失去了在构造函数本身中定义 lambda 的能力,所以我也可以使用带有 operator() 的类。我想在构造函数中定义比较函数 见上文。你可以让它成为你班级的静态成员。 评论编辑(代码示例):看代码示例,不一样。您的建议不起作用,因为 lambda 不能在未评估的上下文中使用 + 无法正确推断类型,因为它是编译器为您创建的每个单独的 lambda 生成的。我还发现了相关的 SO 问题,他们就是这么说的。否则我只会使用 lambda... 对。只是在测试它。删除了那一点。 @cfa45ca55111016ee9269f0a52e771 lambda 类型仅取决于返回类型和参数类型。您可以使用任何具有相同返回值和参数的 lambda。您可以使用一个小样本 lambda 在其上使用 decltype:decltype([](bool A, bool B)return bool(1))`。【参考方案2】:

是的,std::function 向您的set 引入了几乎不可避免的间接寻址。虽然编译器在理论上总是可以找出所有使用你的setstd::function 都涉及在一个总是完全相同的 lambda 的 lambda 上调用它,它既坚硬又极其脆弱。

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

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

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

所以是的,实际上我怀疑std::function 可能会更慢。另一方面,std::function 解决方案比 make_set 解决方案更易于维护,并且用程序员的时间换取程序性能是相当可替代的。

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

如果您创建了一个静态或全局无状态 lambda auto MyComp = [](A const&amp;, A const&amp;)-&gt;bool ... ,您可以使用 std::set&lt;A, decltype(MyComp)&gt; 语法创建一个可以持续存在但易于编译器优化的 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 算法。

【讨论】:

关于备用容器的好建议,但要注意一些其他类型的散列函数(ints 可以正常工作)。如果操作不当,可能会影响性能——无论是速度太慢还是碰撞次数过多。 我想我错过了 make_set 的这一点,它不适用于 lambdas。只剩下 std::function 解决方案,它确实涉及间接但目前我没有性能问题。向量也很有趣,设置的内容是由 GUI 读取和渲染的,因此渲染发生的频率要高得多,因为用户更改了内容(这种情况很少发生)......也许在每次更改时对向量进行排序实际上会比键查找更快 auto functor = [](...)...; 语法的优点是比 struct functor bool operator()(...)const...; ; 语法短,缺点是需要 functor 实例才能调用它(与任何默认构造的仿函数相反struct 案例)。【参考方案3】:

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

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

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

【讨论】:

有趣,我不知道它转换为函数指针......至于标准/非标准,我的意思是如果我依赖 std::function 将来会更好地实现,那么未来版本的编译器将使其与内联的 lambda 一样快,而无需我更改代码 我明白了。 std::function 的效率是有限度的,但您是否在担心之前实际测量过是否存在性能问题? std:function 可以非常快:timj.testbit.eu/2013/01/25/cpp11-signal-system-performance @fr33domlover:您的假设不一定正确。 std::function 的定义需要对所持有的实际 callable 实体进行类型擦除,这几乎需要间接寻址。即使在普通函数指针的情况下,也会比为特定目的创建仿函数的成本更高。这正是std::sort 比C 的qsort 快的原因。【参考方案4】:

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

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

因为std::function 通常有点太重了。不过,我无法评论您的具体情况,因为我不知道。

【讨论】:

看起来像一个优秀的通用解决方案,但我只需要我的 std::set 小案例,我更喜欢只使用 make_set ,它是通用委托类的“特例” .但总的来说它很有趣,也许可以解决所有这些 lambda 问题 这样的德尔门在无状态函子上仍然有一层不透明的间接(即,相当于指针解引用)。使用无状态函子的能力是 std::sort 优于 qsort 的重要原因之一。 哦,如果它有非内联间接,我也可以使用 std::function 并获得相同的速度...... 我可能无法使用gdb,但在我看来,当我使用 -O3 的委托时,编译器通常会消除所有取消引用。【参考方案5】:

如果您确定将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 具有适当的转换函数。这当然会涉及到函数指针的间接寻址。

【讨论】:

【参考方案6】:

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

【讨论】:

我使用 GCC,但我想知道流行的编译器通常会做什么。可能的间接性是我不简单选择 std::function 解决方案的原因 ***.com/questions/8298780/…,那个可能很有趣。 嗯...它说编译器仍然没有完全处理 std::function 的简单情况

以上是关于C++11 std::set lambda 比较函数的主要内容,如果未能解决你的问题,请参考以下文章

如何在 C++ std::set 中放置看起来不可比较的对象?

std::set 插入器不尊重自定义比较器。 (可能的编译器错误?)

具有个人比较功能的 std::set 具有相同的值[重复]

C++ std::set 取第一个元素

C++11——lambda表达式

c_cpp std :: map和std :: set