c++ - std::min(0.0, 1.0) 和 std::max(0.0, 1.0) 会产生未定义的行为吗?
问题描述
这个问题很清楚。下面给出了我认为这些表达式可能会产生未定义行为的原因。我想知道我的推理是对还是错以及为什么。
短读:
(IEEE 754)double
不是Cpp17LessThanComparable因为<
不是严格的弱排序关系,因为NaN
. 因此,违反了和的Requires元素。std::min<double>
std::max<double>
长读:
所有参考都遵循n4800。std::min
和的规格std::max
在 24.7.8 中给出:
template<class T> constexpr const T& min(const T& a, const T& b);
template<class T> constexpr const T& max(const T& a, const T& b);
要求:[...] 类型 T 应为Cpp17LessThanComparable(表 24)。
表 24 定义了Cpp17LessThanComparable并表示:
要求:
<
是严格的弱排序关系(24.7)
第 24.7/4 节定义了严格的弱排序。特别是,因为<
它指出“如果我们定义equiv(a, b)
为!(a < b) && !(b < a)
thenequiv(a, b) && equiv(b, c)
意味着equiv(a, c)
”。
现在,根据 IEEE 754 equiv(0.0, NaN) == true
,equiv(NaN, 1.0) == true
但equiv(0.0, 1.0) == false
我们得出结论,这不是<
严格的弱排序。因此, (IEEE 754)不是Cpp17LessThanComparable,这违反了and的Requires子句。double
std::min
std::max
最后,15.5.4.11/1 说:
违反函数的Requires:元素中指定的任何先决条件会导致未定义的行为 [...]。
更新1:
问题的重点不是争论那个std::min(0.0, 1.0)
是未定义的,当程序评估这个表达式时,任何事情都可能发生。它返回0.0
。时期。(我从来没有怀疑过。)
关键是要显示标准的(可能的)缺陷。在对精确度的值得称赞的追求中,该标准经常使用数学术语,弱严格排序只是一个例子。在这些情况下,数学精度和推理必须一路走好。
例如,请看 Wikipedia 对严格弱排序的定义。它包含四个要点,每个要点都以“For every x [...] in S...”开头。他们都没有说“对于 S 中对算法有意义的某些值 x”(什么算法?)。此外,规范中std::min
明确表示“T
应为Cpp17LessThanComparable ”,这意味着<
对T
. 因此,在 Wikipedia 页面中扮演集合 S 的角色,当整体考虑 的T
值时,四个要点必须成立。T
显然,NaN 与其他双精度值完全不同,但它们仍然是可能的值。我在标准中没有看到任何东西(它很大,1719 页,因此这个问题和语言律师标签)在数学上得出的结论std::min
是,只要不涉及 NaN,双打就可以了。
实际上,有人可以争辩说 NaN 很好,而其他双打是问题所在!事实上,回想一下,有几个可能的 NaN 双精度值(其中 2^52 - 1 个,每个都携带不同的有效负载)。考虑包含所有这些值和一个“正常”双精度值的集合 S,例如 42.0。在符号中,S = { 42.0, NaN_1, ..., NaN_n }。事实证明,这<
是对 S 的严格弱排序(证明留给读者)。C++ 委员会在指定std::min
为“请不要使用任何其他值,否则严格的弱排序被破坏并且行为std::min
未定义”时是否考虑了这组值?我敢打赌不是,但我更愿意在标准中阅读此内容,而不是推测“某些值”的含义。
更新 2:
std::min
将(上)的声明与clamp
24.7.9的声明进行对比:
template<class T> constexpr const T& clamp(const T& v, const T& lo, const T& hi);
要求: 的值lo
不得大于hi
。对于第一种形式,类型 T 应为Cpp17LessThanComparable(表 24)。[...]
[注意:如果NaN
避免, T 可以是浮点类型。——尾注]
在这里,我们清楚地看到“std::clamp
只要不涉及 NaN 就可以使用双打”。我正在寻找相同类型的句子std::min
。
值得注意的是 Barry 在他的帖子中提到的 [structure.requirements]/8 段。显然,这是在来自P0898R0的 C++17 之后添加的):
本文档中定义的任何概念所需的操作不必是全部功能;也就是说,所需操作的某些参数可能会导致无法满足所需的语义。[示例:StrictTotallyOrdered
<
概念 (17.5.4) 的必需运算符在对 NaN 进行操作时不满足该概念的语义要求。— 结束示例] 这不会影响类型是否满足概念。
这是解决我在这里提出的问题的明确尝试,但在概念的背景下(正如 Barry 所指出的,Cpp17LessThanComparable不是一个概念)。此外,恕我直言,这一段也缺乏精确性。
解决方案
在新的[concepts.equality]中,在稍微不同的上下文中,我们有:
如果给定相等的输入,表达式会产生相等的输出,则表达式是保持等式的。表达式的输入是表达式操作数的集合。表达式的输出是表达式的结果和表达式修改的所有操作数。
并非所有输入值都需要对给定表达式有效;例如,对于整数
a
和b
,表达式在isa / b
时定义不明确。这并不排除表达式是保持平等的。表达式的域是需要明确定义表达式的一组输入值。b
0
a / b
虽然表达式域的概念并未在整个标准中完全表达,但这是唯一合理的意图:句法要求是类型的属性,语义要求是实际值的属性。
更一般地说,我们还有[structure.requirements]/8:
本文档中定义的任何概念所需的操作不必是全部功能;也就是说,所需操作的某些参数可能会导致无法满足所需的语义。[ <em>示例:在对s进行操作时,概念的必需
<
运算符StrictTotallyOrdered
([concept.stricttotallyordered]) 不满足该概念的语义要求。NaN
— <em>end example ] 这不会影响类型是否满足概念。
这具体指的是概念,而不是像Cpp17LessThanComparable这样的命名需求,但这是理解库如何工作的正确精神。
当Cpp17LessThanComparable给出语义要求时
<
是严格的弱排序关系 (24.7)
违反这一点的唯一方法是提供一对违反严格弱排序要求的值。对于像这样的类型double
,那就是NaN
. min(1.0, NaN)
是未定义的行为——我们违反了算法的语义要求。NaN
但是对于没有,的浮点数<
是一个严格的弱排序 - 所以这很好......你可以使用min
, max
, sort
, 任何你喜欢的。
展望未来,当我们开始编写使用 的算法时,operator<=>
域的概念是表达句法要求的原因之一ConvertibleTo<decltype(x <=> y), weak_ordering>
是错误的要求。be很好,它x <=> y
只是partial_ordering
看到了一对 is 不是的值x <=> y
(partial_ordering::unordered
至少我们可以通过 诊断[[ assert: (x <=> y) != partial_ordering::unordered ]];
)