对关键容器的键类型(例如std :: map)的比较器的要求是它对键类型的元素施加严格的弱顺序。
对于给定的比较器comp(x, y)
,我们定义equiv(x, y) = !comp(x, y) && !comp(y, x)
comp(x, y)
作为严格弱订单的要求是
!comp(x, x)
所有x
)comp(a, b)
和comp(b, c)
然后comp(a, c)
)。equiv(a, b)
和equiv(b, c)
然后equiv(a, c)
) std::less<float>
(默认比较器)使用operator<
,由于NaN
,它不会创建严格的弱顺序。由于x < NaN
和NaN < x
对所有x
都是假的,因此NaN
相当于此比较器下的所有浮点数,这会打破条件#3:equiv(1.0, NaN)
和{{ 1}}但不是equiv(NaN, 2.0)
。对于除NaN之外的IEEE浮点数,它是一个严格的弱顺序(其中每个数字都有自己的等价类,除了equiv(1.0, 2.0)
和0
)。
这是否意味着C ++标准不允许使用IEEE浮点数(和(长)双精度数)作为关联容器中的键类型,因为上述问题,即使我确保NaN永远不会插入进入容器?我不太确定标准中的“-0
”措辞的元素 - 如果它意味着所有可能的元素或仅仅是容器中最终的元素。
注意:问题不在于问题。截断/舍入,我很快就会发布一个不同的问题。
嗟。我应该在没有指定浮点数的情况下提出问题,我只是觉得这是一个很好的例子。
问题确实是:是否允许使用仅对放入容器的元素施加严格弱顺序的比较器,而不是所有可能的键类型实例?请不要只回答“是”或“否”,我想参考标准/先前关于此事的讨论/来自委员会成员的答案。
答案 0 :(得分:9)
我怀疑这些限制应该被视为关系对实际用作键的值的行为,而不一定是所有类型的值。目前没有时间通过标准寻找引用实际容器元素的“吸烟枪”语言,而不是所有类型的值。
类似的情况:如果一个比较器(用于指针或智能指针的容器)调用一个虚函数,并且有人链接它所比较的类型的派生类,它会以一种使比较器没有的方式覆盖虚函数一个严格的弱势秩序?即使没有人真正使用该派生类,该程序是否也未定义?
如果有疑问,你可以用一个严格弱的比较器来支持NaN:
bool operator()(double a, double b) {
if ((a == a) && (b == b)) {
return a < b;
}
if ((a != a) && (b != b)) return false;
// We have one NaN and one non-NaN.
// Let's say NaN is less than everything
return (a != a)
}
最后两行“优化”到return (b == b);
,虽然我不确定评论是否会优化。
我认为Tomalak已经说服我,语言确实说需要订购整个类型。
这没有多大意义,因为地图不会冒出任何值,它只使用它给出的值(以及它们的副本),但问题是关于规则,它们是规则,就我而言知道。 C ++ 0x是一样的。我想知道是否有缺陷报告或任何提交缺陷的报告。
在std::less
指针慢的(非常罕见的)系统中,你不能使用<
作为指针映射中的比较器,这也很烦人即使你知道指针都是同一个数组的元素。羞。
另一种选择是使用以下类作为键类型,因此仅在进入地图时检查键是否为NaN,而不是如上所述的每次比较。
struct SaneDouble {
double value;
SaneDouble (double d) : value(d) {
if (d != d) throw std::logic_error();
}
static friend bool operator<(SaneDouble lhs, SaneDouble rhs) {
return lhs.value < rhs.value;
}
// possibly a conversion to double
};
这提出了另一个问题 - 显然有人可以创建SaneDouble
,然后将其value
设置为NaN(假设实现允许他们从某处获得一个而不会崩溃)。那么“SaneDouble的元素”是否严格弱有序?在构造函数中创建类不变量的半心半意的尝试是否使我的程序未定义,即使没有人真正打破不变量,只是因为它们可以因此这样做的结果是“SaneDouble的元素” ?当且仅当value
被标记为private
时,是否真的有标准的意图才能定义程序的行为?标准是否实际定义了某种类型的“元素”在哪里?
我想知道我们是否应该将“Key的元素”解释为比较器对Key的某些元素产生严格的弱序。大概包括实际使用的那些。 “我有甜甜圈”并不意味着我有每个甜甜圈。不过,这是一段时间。
答案 1 :(得分:2)
简而言之:这很好(就你的问题而言)。
如果您阻止不符合排序要求的值(即NaN
),则完全定义行为。
答案 2 :(得分:1)
将浮点数作为关联容器的键有时是一个坏主意,因为相等语义非常差。但这取决于你想做什么。请记住,NaN和无穷大通常不是问题。您可以使用特殊的比较器功能处理它们(我通常不会这样做),显然标准的要求是关于实际的密钥,它们最终会在容器中,您可以看作<密钥类型的strong>子集。地图实现永远不会引入您未将自己提供给地图的关键实例。
我曾经将这个谓词用于地图,我可以禁用两个非常接近的值:
struct FuzzyComparer
{
template <typename T>
bool operator()(T x, T y) const
{
static const T oneMinusEps = (T)1. - 64 * std::numeric_limits<T>::epsilon();
return x / y < oneMinusEps;
}
};
这并不能为您提供良好的等价关系。这仅在您想要存储离散浮点值时才有用,并且您已准备好容忍计算中的某些舍入错误,从而产生您要检索的键。 对于要插入的实际密钥,它会产生等价关系。
你不会能够在与算术运算兼容的浮点数上找到一个良好的等价关系,即。使加法和乘法相关联。
你要么必须抛弃“等价关系”部分,这在现实世界的代码中不应该是一个大问题(我怀疑eq。关系的传递性在某种程度上会使你感到麻烦典型的地图实现)或抛弃与算术运算的兼容性。但是,将浮点值用作键是什么意思呢?
答案 3 :(得分:0)
您可以将浮点数和双精度数作为std :: map或std :: set的键,以便正确排序。
问题在于它的独特性,因为你可能会因浮动和双打的比较而发生重复。
使用基于epsilons的比较(接近的值被认为是相等的)也不是没有危险,因为你可能因为太近而消除了真正的非重复。
如果你使用简单的“查找”,“查找”可能找不到那里的元素,所以你可能希望在x-delta上使用某种类型的epsilon查找和upper_bound(),其中x是你真正看的东西for,并允许一个小于x + delta的值。
考虑到所有这些,显然没有问题,只要你使用upper_bound来搜索而不是使用equal_range,就可以在std :: multiset或std :: multimap中使用float或double作为键。
关于NaN,如果集合或映射不为空,则它们将被认为与已存在的任何元素“相等”,因此不会插入。如果先插入NaN,则所有后续插入都将失败。
他们被认为是平等的,因为x<NaN
和NaN<x
都评估为假。
当然,如果它是一张地图,你会调用相同的逻辑
myMap[ NaN ] = x;
它可以合理地修改任何元素。
(如果执行find(NaN)
则相同,可以返回任何迭代器)。
因此,如果您要在此计算的任何部分使用NaN,请使用特殊的比较器,例如Steve Jessop。