【问题标题】:Is NaN a valid key value for associative containers?NaN 是关联容器的有效键值吗?
【发布时间】:2011-12-27 03:56:15
【问题描述】:

考虑在 C++ 中键入 double 的有序和无序关联容器。

NaN 是有效的密钥类型吗?

对于有序容器,我应该说“不”,因为它不尊重严格的弱排序。

对于无序容器,我不知道。

以下是 GCC 4.6.2 中发生的情况:

#include <map>
#include <unordered_map>

#include <cmath>

#include <iostream>
#include <prettyprint.hpp>

int main()
{
  typedef std::map<double, int> map_type; // replace by "unorderd_map"

  map_type dm;
  double d = std::acos(5); // a good nan

  dm[d] = 2;
  dm[d] = 5;
  dm[d] = 7;

  std::cout << "dm[NaN] = " << dm[d] << ", dm = " << dm << std::endl;
}

对于有序地图,我得到:

dm[NaN] = 7, dm = [(nan, 7)]

对于无序地图,我得到:

dm[NaN] = 0, dm = [(nan, 0), (nan, 7), (nan, 5), (nan, 2)]

所以在有序映射中,所有 NaN 都被同等对待,这是我所期望的,尽管 NaN 似乎会违反要求。然而,对于无序映射,我永远无法再次检索元素,并且所有 NaN 都是不同的。这也不是我所期望的。

标准对这个问题有什么要说的吗?

更新:感谢下面的出色答案,请注意,如果您在其中插入 任何其他内容,std::map 会在其中插入 NaN 时中断。 p>

(非常感谢 cmets 了解其他语言如何处理关联容器中的浮点键。)

【问题讨论】:

  • 标准对 NaN 并没有什么特别的说法,因为 NaN 是一个 IEEE 概念。换句话说,它依赖于平台。
  • @JohnDibling:这是一个很好的观点,尽管标准承认数字类型可能具有“nan”值;如&lt;limits&gt;。
  • > 考虑 C++ 中的有序和无序关联容器,键为 double。 > NaN 是有效的密钥类型吗?错误的问题。正确的问题是:内置小于的哪个可能的域包含 NaN?

标签: c++ map nan unordered-map


【解决方案1】:

这是因为

std::less<double>(NaN, NaN) == false

就像你说的,weak total ordering(std::map 需要)是可以的,相等(或 equivalence,对任何哈希的额外要求-基于容器)不能满足散列(无序)映射的关键要求

Irreflexivity   f(x, x) must be false. 
 Antisymmetry   f(x, y) implies !f(y, x) 
 Transitivity   f(x, y) and f(y, z) imply f(x, z). 
 Transitivity of equivalence     

         Equivalence (as defined above) is transitive: if x 
         is equivalent to y and y is equivalent to z, then x is 
         equivalent to z. (This     implies that equivalence does 
         in fact satisfy the mathematical definition of an equivalence 
         relation.)

看到对于 std::map,等价是当 !less(a,b) &amp;&amp; !less(b,a) 时,我会说满足所有约束。

【讨论】:

  • 是的,我很欣赏这一点,但是标准中是否以某种方式记录或评论了这种行为?另外,我不会说“排序没问题”,因为 NaN not 服从 SWO:它 not 是 { less, greater, equal } 之一与任何事物相比。
  • @KerrekSB 查看SGI std::map 并点击进入SGI Strict Weak Ordering 页面
  • NaN 值失败的 SWO 部分是等价的传递性。 0 &lt; NaN 和 NaN &lt; 0 都是错误的。 1 &lt; NaN 和 NaN &lt; 1 都是假的。因此,0 &lt; 1 和1 &lt; 0 都应该是假的,但它们不是。因此,只要您将 NaN 放入其中,该地图就会具有未定义的行为。可以说它甚至更早有 UB,因为标准规定订单必须是键 type 上的 SWO,而不仅仅是实际添加到地图的键值上,但这是一个很好的点。
  • 嗯...我明白了:鉴于 NaN 永远不会小于,因此这些条件是空洞的。好没问题。所以对于map,double 是完全合法的密钥类型。
  • @Steve:是的,我搞砸了。 Kerrek:如果此时您尝试在地图中插入 除了 NaN 之外的任何内容,您的测试将失败——它将被视为等同于 NaN,因此不会被插入。
【解决方案2】:

NaNs 可以存储在地图中——也就是说,它们是可复制构造的,并且不具有可比性。 std::less for doubles 不符合 map 对严格弱排序的要求,因此从技术上讲,您在这里有未定义的行为。但是,即使标准没有要求,这种行为也很容易解释。 Map 使用等价而不是相等来确定一个项目是否是重复的。两个 NaN 比较相等,但不相等。但是,这在某些情况下会分崩离析。例如,如果您尝试在该映射中插入 除了 NaN 之外的东西,它将被视为等同于 NaN,并且您不会得到任何插入。尝试在 NaN 之外添加一些实数,您也会看到地图出现故障。

哈希行为是预期的,但也没有为哈希表定义——哈希表要求它们的内容是可复制构造的并且相等性可比较。多个 NaN 的哈希比较相等,所以它们都将进入同一个桶,但是哈希表使用相等比较而不是小于比较(相等而不是等价)。因此,没有一个 NaN 比较彼此相等,并且您会为该键获得多个插入。这是因为 NaN 打破了哈希表的等式可比要求——即 std::equal_to(x, x) 为真的不变量。

【讨论】:

  • 是的,干杯...我可以看到所有这些东西;我只是想知道这是否是标准中公认的角落;或者如果每个编译器供应商都应该说“不要在我们的关联容器中使用 NaN 值”。
  • @KerrekSB:我认为在地图的情况下,由于另一个答案中提到的 SWO 问题,您在技术上存在未定义的行为。我认为哈希表的行为实际上是很好定义的。我不认为标准明确说明了这些。它只是为了满足可哈希、相等可比、小于可比和严格弱排序的要求而出现的。
  • @KerrekSB:换句话说,我想不出任何符合标准的 map 和 unordered_map 的实现与您在测试中显示的行为有任何不同,尽管标准没有明确提及。跨度>
  • 我在想你必须提供一个不同的谓词函子,它继承 equal_to
  • 哈希表中 NaN 的问题是查找总是返回“false”。所以你可以插入元素,但你永远找不到它......你确定unordered_map 不需要operator==(x,x) 是真的吗?
【解决方案3】:

它们都被标准禁止。

对于(有序的)关联容器,严格弱序的定义(25.4/4)说:

如果我们将equiv(a, b) 定义为!comp(a, b) &amp;&amp; !comp(b, a),那么 要求是comp 和equiv 都是传递关系... equiv(a, b) &amp;&amp; equiv(b, c) 暗示 equiv(a, c)

这对于 a = 0.0、b = NaN、c = 1.0、comp = std::less&lt;double&gt;() 会失败

对于无序容器,23.2.5/3 表示相等谓词Pred“在Key 类型的值上引入等价关系”。等价关系是自反的,std::equal_to&lt;double&gt;()(NaN,NaN) 为假,所以equal_to&lt;double&gt;() 不是等价关系。

顺便说一句,在 double 上键入容器有点可怕,就像比较 double 是否相等总是有点可怕一样。你永远不知道你会在最低有效位中得到什么。

我一直认为有点奇怪的是,该标准根据键 type 来表达要求,而不是根据添加到容器中的实际键值来表达要求。我相信您可以选择将其阅读为不保证 map&lt;double, int&gt; 在实现支持 NaN 的情况下完全定义了行为,无论您是否实际将 NaN 添加到实例中。但在实践中,std::map 的实现无法以某种方式从其后袋中召唤出NaN 并尝试比较它,它只会比较传递给实例的键值。因此,如果您避免添加 NaN,应该没问题(如果有点吓人)。

非常感谢 cmets 了解其他语言的处理方式 关联容器中的浮点键

在 Python 中的一些快速实验(其中 set 和 dict 是通过引用保存键和值的无序关联容器)表明 NaN 被视为值不相等的对象,即使它们“相同” NaN”,但同样的 nan object 可以通过身份再次找到。据我所见,容器似乎并没有因为包含多个 nan 或 nan 和其他值的混合而受到干扰:

>>> thing = set()
>>> nan = float('nan')
>>> nan
nan
>>> thing.add(nan)
>>> thing.add(nan)
>>> thing
set([nan])

>>> thing = dict()
>>> thing[nan] = 1
>>> thing[nan] = 2
>>> thing[nan]
2
>>> nan2 = float('nan')
>>> thing[nan2] = 3
>>> thing
{nan: 2, nan: 3}

>>> thing = set()
>>> thing.add(nan)
>>> thing.add(nan2)
>>> thing
set([nan, nan])

>>> thing = dict()
>>> thing[nan] = 1
>>> thing[nan2] = 2
>>> thing[0] = 3
>>> thing
{nan: 1, nan: 2, 0: 3}
>>> thing.keys()
[nan, nan, 0]
>>> thing.values()
[1, 2, 3]
>>> thing[0]
3
>>> thing[1]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 1

【讨论】:

  • 答案的威权语气唤起了某种“接受”反射,但我会再等一会儿:-)
  • 我遗漏的一件事是,如果NaN 是您添加到map 的唯一值,那么less&lt;double&gt;() 是仅对该值的严格弱排序。在大小为 1 的域上编写一个作为 SWO 工作的函数并不难,唯一的要求是返回 false。事实上,我认为它在失败之前需要一个 NaN 和 两个 不同的正常值:对于 NaN 和一个值,它应该表现得好像 NaN 等同于该值。但是这样的地图可能不是很有趣……
  • @Steve:我认为您的两元素示例的行为就像 NaN 与另一个值等于一样。 (两者都不小于另一个 => 它们是等价的。)这意味着您将永远不会真正看到地图中的另一个元素 :-)
  • @Nemo:同意,事实上我更正了评论,但我想我们在邮件中越过了。
  • 那个 Python 例子实在是太奇怪了。我找不到任何合理的心理模型可以让这种行为有意义。
猜你喜欢
  • 2011-09-21
  • 1970-01-01
  • 1970-01-01
  • 2010-11-12
  • 1970-01-01
  • 2017-07-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多