NaN是关联容器的有效键值吗?

时间:2011-11-11 16:14:54

标签: c++ map nan unordered-map

考虑使用double上的C ++中的有序和无序关联容器。

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将会中断。

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

2 个答案:

答案 0 :(得分:18)

标准禁止使用它们。

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

  

如果我们将equiv(a, b)定义为!comp(a, b) && !comp(b, a),那么   要求是compequiv都是传递关系......   equiv(a, b) && equiv(b, c)隐含equiv(a, c)

a = 0.0,b = NaN,c = 1.0,comp = std::less<double>()

失败

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

顺便说一下,在双精度上键入容器有点可怕,就像比较双精度相等总是有些可怕一样。你永远不会知道你最不重要的是什么。

我一直认为有点奇怪的是标准表达了关键类型方面的要求,而不是根据添加到容器的实际键值。我相信你可以选择阅读这一点,因为如果实现支持NaN,无论是否实际向某个实例添加NaN,都不能保证map<double, int>已经定义了行为。但实际上,std::map的实现不能以某种方式从后袋中召唤NaN并试图比较它,它只会比较传递给实例的键值。所以它应该没问题(如果有点吓人),你可以避免添加NaN。

  

我非常感谢有关其他语言如何处理的评论   关联容器中的浮点键

Python中的一些快速实验(其中setdict是无序的关联容器,通过引用保存键和值)表明NaN被视为价值不相等的对象,即使它们'重新“相同的NaN”,但同样的nan 对象可以通过身份再次找到。据我所见,容器似乎不会被包含多个nans或nans和其他值的混合物所扰乱:

>>> 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

答案 1 :(得分:3)

NaN可以存储在地图中 - 也就是说,它们是可复制的,并且可以比较。 std::less对于双打不符合地图对严格弱排序的要求,因此您在技术上得到了未定义的行为。但是,即使标准不要求,也很容易解释这种行为。 Map使用等价而不是相等来确定项是否重复。两个NaN比较等效,但不相等。但是,在某些情况下会崩溃。例如,如果您尝试将 NaN之外的内容插入到该地图中,则会将其视为与NaN等效,并且您不会获得插入内容。尝试添加一些除NaN之外的实数,你也可以看到地图分解。

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