【问题标题】:Why use std::less as the default functor to compare keys in std::map and std::set?为什么使用 std::less 作为默认函子来比较 std::map 和 std::set 中的键?
【发布时间】:2014-04-14 01:03:57
【问题描述】:

我想知道为什么 std::mapstd::set 使用 std::less 作为默认函子来比较键。为什么不使用类似于 strcmp 的仿函数呢?比如:

  template <typename T> struct compare
  {
     // Return less than 0 if lhs < rhs
     // Return 0 if lhs == rhs
     // Return greater than 0 if lhs > rhs
     int operator()(T const& lhs, T const& rhs)
     {
        return (lhs-rhs);
     }
  }

假设map 中有两个对象,键为key1key2。现在我们要插入另一个带有 key3 键的对象。

使用std::less时,insert函数需要先用key1key3调用std::less::operator()。假设 std::less::operator()(key1, key3) 返回 false。它必须在切换键后再次调用std::less::operator()std::less::operator()(key3, key1),以决定key1 是否等于key3key3 是否大于key1。如果第一个调用返回 false,则对 std::less::operator() 进行两次调用以做出决定。

如果std::map::insert 使用了compare,则只需一次调用即可获得足够的信息来做出正确的决定。

根据 map 中键的类型,std::less::operator()(key1, key2) 可能很昂贵。

除非我遗漏了一些非常基本的东西,否则std::mapstd::set 不应该使用类似compare 而不是std::less 作为比较键的默认函子吗?

【问题讨论】:

  • Doesnt std::setstd::map 是底层的 RB-Trees?
  • 但这意味着必须为任何希望使用映射或集合的数据类型重载- 运算符
  • @Smac89,这是真的。同样正确的是你现在必须实现operator&lt;
  • @Paranaix 我认为这在技术上是一个实现细节。但是,如果您深入到标准规范的底部,您可能会发现很难找到另一个仍然满足标准 的性能限制的实现指定。
  • @Paranaix,该标准不要求任何特定的结构,只是性能保证。

标签: c++ stdmap stdset


【解决方案1】:

我决定就此询问 Alexander Stepanov(STL 的设计师)。我可以这样引用他的话:

最初,我提出了 3 路比较。标委会要求 我改为标准比较运算符。我做了我被告知的事情。 我一直主张在标准中添加 3 路组件 超过 20 年。

但请注意,也许不直观,2-way 并不是一个巨大的开销。 您不必进行两倍的比较。 在下降过程中每个节点只进行一次比较(无相等性检查)。代价是不能及早返回(当密钥在非叶子中时)和最后一次额外的比较(交换参数以检查相等性)。如果我没记错的话,那就是

1 + 1/2*1 + 1/4*2 + 1/8*3 + ...
= 1 + 1/2+1/4+1/8+... + 1/4+1/8+... + ...
-> 3  (depth -> infty)

平均在包含查询元素的平衡树上进行额外比较。

另一方面,3 路比较没有可怕的开销:Branchless 3-way integer comparison。现在,在每个节点上检查比较结果与 0(相等)的额外分支是否比最后支付约 3 次额外比较的开销更少,这是另一个问题。应该没多大关系吧。但我认为比较本身应该是 3 值的,因此可以改变是否使用所有 3 个结果的决定。

更新:请参阅下面的 cmets,了解为什么我认为 3 路比较在树中更好,但不一定在平面数组中。

【讨论】:

  • 最初,我提出了 3 路比较。 从您的帖子中不清楚他是希望它们作为通用仿函数还是他想在 @987654324 的实现中使用它们@ 和 std::set.
  • 哦。这很不错。马的嘴。无法反驳:)喜欢这个答案
  • @RSahu:你的意思是:他是否认为三向比较在树中特别有用?在每个节点上实际测试相等性是否更好,因为它使用 3-way 很便宜并且使我们能够早点返回?他没有具体回应。
  • @RSahu:但实际上我认为在搜索树中 is 的节点时访问 2-ish 节点的能力更少,对于真实世界的深度 RB 树~20,值得。因为它节省了 2 次缓存未命中。不过,我还没有对其进行基准测试。
  • 看起来 3-way compare 即将进入 c++,形式为 operator&lt;=&gt;,见 open-std.org/JTC1/SC22/WG21/docs/papers/2017/p0515r0.pdf
【解决方案2】:

基于树的容器只需要严格的弱总排序。

https://www.sgi.com/tech/stl/StrictWeakOrdering.html

  1. 写权限

    map 和 sets 的插入点完全由单个二进制搜索确定,例如lower_boundupper_bound。二分查找的运行时复杂度为O(log n)

  2. 读取权限

    这同样适用于搜索:搜索比线性等式扫描效率高得多,正是因为大多数元素需要比较。诀窍是容器是有序的。


结果是equality 信息不需要存在。只是,这些项目可以有等价排序

实际上,这只是意味着对元素类型的限制更少,实现需求的工作更少,并在常见使用场景中实现最佳性能。总会有取舍。 (例如,对于大型集合,哈希表(无序集合和映射)通常更有效。请注意,这些 do 需要 equatable 元素,并且它们采用哈希方案快速查找)

【讨论】:

  • 一个返回&lt; 0== 0&gt; 0的比较函数对于any比较函数less是可以实现的,只要返回0 if @987654332 @。这将是完全具有表现力的,没有任何类型可以具有可用的less 函数但不能具有可用的compare 函数。
  • @hvd 这是一个重言式。然而,在实践中,operator= 通常用于表达相等语义,而不是等价。将两者混合起来会非常令人惊讶。并非所有具有总排序的类型都必须实现相等。
  • 是的,你是对的,我已经编辑了我之前的评论以删除我对operator== 的引用。但是比较两个项目并根据比较结果返回负数、零或正数的函数已经很常用,其中零并不意味着相等。一个非常常见的例子是不区分大小写的字符串比较。
  • 好点,@hvd。你说得对。那是Comparable 的概念,比LessThanComparable 有更多的要求
  • @sehe,我相信你的意思是operator==
猜你喜欢
  • 2017-07-28
  • 1970-01-01
  • 1970-01-01
  • 2012-01-10
  • 2015-10-11
  • 2012-01-12
  • 2013-09-13
  • 1970-01-01
相关资源
最近更新 更多