std :: min(0.0,1.0)和std :: max(0.0,1.0)是否会产生未定义的行为?

问题描述 投票:50回答:3

问题很清楚。以下给出了我认为这些表达式可能产生未定义行为的原因。我想知道我的推理是对还是错,为什么。

简短阅读:

(IEEE 754)double不是Cpp17LessThanComparable,因为由于<NaN不是严格的弱排序关系。因此,违反了std::min<double>std::max<double>的Requires元素。

长读:

所有参考文献都遵循n4800std::minstd::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),那么equiv(a, b) && equiv(b, c)意味着equiv(a, c)”。

现在,根据IEEE 754 equiv(0.0, NaN) == trueequiv(NaN, 1.0) == trueequiv(0.0, 1.0) == false我们得出结论,<不是一个严格的弱序。因此,(IEEE 754)double不是Cpp17LessThanComparable,它违反了std::minstd::max的Requires子句。

最后,15.5.4.11/1说:

违反函数的Requires:元素中指定的任何前提条件会导致未定义的行为[...]。

更新1:

问题的关键不在于std::min(0.0, 1.0)未定义,当程序评估此表达式时,任何事情都可能发生。它返回0.0。期。 (我从来没有怀疑过。)

关键是要显示标准的(可能的)缺陷。在一个值得称道的精确追求中,标准通常使用数学术语,而弱严格排序只是一个例子。在这些场合,数学精确度和推理必须一直持续下去。

例如,看看维基百科对strict weak ordering的定义。它包含四个子弹点,每个子弹点都以“对于S中的每个x [...]”开头。他们都没有说“对于算法中有意义的S中的某些值x”(什么算法?)。此外,std::min的规范明确指出“T应该是Cpp17LessThanComparable”,这意味着<是对T的严格弱序。因此,T在维基百科的页面中扮演集合S的角色,当整个考虑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可以是浮点类型。 - 结束说明]

在这里,我们清楚地看到一些内容,“只要不涉及NaN,std::clamp就可以完成双打。”我正在为std::min寻找相同类型的句子。

值得注意Barry在他的post中提到的段落[structure.requirements] / 8。显然,这是来自P0898R0的后C ++ 17添加的:

本文档中定义的任何概念的必要操作不必是总函数;也就是说,所需操作的某些参数可能导致无法满足所需的语义。 [示例:在NaN上操作时,StrictTotallyOrdered概念(17.5.4)所需的<运算符不符合该概念的语义要求。 - 结束示例]这不影响类型是否满足概念。

这是一个明确的尝试,以解决我在这里提出的问题,但在概念的背景下(正如Barry所指出的,Cpp17LessThanComparable不是一个概念)。另外,恕我直言这一段也缺乏精确性。

c++ floating-point language-lawyer undefined-behavior c++-standard-library
3个回答
6
投票

免责声明:我不知道完整的C ++标准,我做了一些关于浮点数的研究。我知道IEEE 754-2008浮点数和C ++。

是的,你是对的,这是C ++ 17标准的未定义行为。

简短阅读:

标准并没有说std::min(0.0, 1.0);是未定义的行为,它说constexpr const double& min(const double& a, const double& b);是未定义的行为。这意味着,它没有应用未定义的函数,它是未定义的函数声明本身。与数学上的情况一样:正如您所注意到的那样,在整个IEEE 754浮点数范围内无法实现最小功能。

但未定义的行为并不一定意味着崩溃或编译错误。它只是意味着它没有被C ++标准定义,并且具体地说它可能“在文档中以环境特征的方式在翻译或程序执行期间表现”

为什么你不应该在双打上使用std::min

因为我意识到下面的长读部分会变得无聊,这里有一个玩具的例子,说明比较中NaNs的风险(我甚至不尝试排序算法......):

#include <iostream>
#include <cmath>
#include <algorithm>

int main(int, char**)
{
    double one = 1.0, zero = 0.0, nan = std::nan("");

    std::cout << "std::min(1.0, NaN) : " << std::min(one, nan) << std::endl;
    std::cout << "std::min(NaN, 1.0) : " << std::min(nan, one) << std::endl;

    std::cout << "std::min_element(1.0, 0.0, NaN) : " << std::min({one, zero, nan}) << std::endl;
    std::cout << "std::min_element(NaN, 1.0, 0.0) : " << std::min({nan, one, zero}) << std::endl;

    std::cout << "std::min(0.0, -0.0) : " << std::min(zero, -zero) << std::endl;
    std::cout << "std::min(-0.0, 0.0) : " << std::min(-zero, zero) << std::endl;
}

在使用Apple LLVM版本10.0.0(clang-1000.10.44.4)编译我的macbookpro时(我做了精度,因为,这是未定义的行为,所以理论上这可能在其他编译器上有不同的结果)我得到:

$ g++ --std=c++17 ./test.cpp
$ ./a.out
std::min(1.0, NaN) : 1
std::min(NaN, 1.0) : nan
std::min_element(1.0, 0.0, NaN) : 0
std::min_element(NaN, 1.0, 0.0) : nan
std::min(0.0, -0.0) : 0
std::min(-0.0, 0.0) : -0

这意味着与你可能假设的相反,当涉及NaNs时,std::min不是对称的,甚至是-0.0。并且NaNs不会传播。简短的故事:这确实引起了我之前项目的一些痛苦,在那里我必须实现我自己的min函数,以便按照项目规范的要求正确传播双方的NaNs。因为双打的std::min没有定义!

IEEE 754:

正如您所注意到的,IEEE 754浮点数(或ISO / IEC / IEEE 60559:2011-06,这是C11标准使用的标准,见下文,或多或少复制IEEE754用于C语言)没有严格的弱排序,因为NaNs违反了不可比性的传递性(fourth point of the Wikipedia page

有趣的是,IEE754规范已在2008年(现在命名为IEEE-754-2008)which includes a total ordering function进行了修订。事实是C ++ 17和C11都没有实现IEE754-2008,而是ISO / IEC / IEEE 60559:2011-06

但谁知道呢?也许这将在未来发生变化。

长读:

首先,让我们从the same standard draft you linked(重点是我的)回顾实际上未定义的行为开始:

未定义的行为行为,本文档没有要求

[注1:当本文档省略任何明确的行为定义或程序使用错误的构造或错误数据时,可能会出现未定义的行为。允许的未定义行为包括完全忽略不可预测的结果,在翻译或程序执行期间以环境特征(有或没有发出诊断消息)的特定行为,终止翻译或执行(发布)一条诊断信息)。许多错误的程序结构不会产生未定义的行为;他们需要被诊断出来。对常量表达式的求值从未表现出在本文件(7.7)的第4条至第14条中明确指定为未定义的行为。 - 尾注]

没有“屈服”未定义的行为。它只是C ++标准中未定义的内容。这可能意味着您可以使用它并获得正确的结果,风险自负(例如,通过执行std::min(0.0, 1.0);或者它可能引发警告甚至编译错误,如果您发现编译器非常小心浮点数!

关于子集......你说:

我没有看到标准中的任何内容(这是一个非常大的,1719页,因此这个问题和语言 - 律师标签)在数学上得出的结论是std :: min对于双精度是好的,只要不涉及NaNs。

我自己也没有读过这个标准,但是从你发布的部分来看,似乎标准已经说过这很好。我的意思是,如果你构造一个新的类型T包含不包括NaN的双精度数,那么应用于你的新类型的template<class T> constexpr const T& min(const T& a, const T& b);的定义将具有一个定义的行为,并且行为与你对最小函数的期望完全一样。

我们还可以查看<上的double操作的标准定义,该定义在25.8浮点类型的数学函数中定义,它表示不太有用:

分类/比较函数的行为与C宏相同,并且C标准库中定义了相应的名称。对于三种浮点类型,每个函数都会重载。另见:ISO C 7.12.3,7.12.4

the C11 standard说什么? (因为我猜C ++ 17不使用C18)

关系运算符和相等运算符支持数值之间通常的数学关系。对于任何有序的数值对,其中一个关系 - less,greater和equal - 都是正确的。当参数值为NaN时,关系运算符可能会引发“无效”浮点异常。对于NaN和数值,或两个NaN,只是无序关系是真的.241)

至于C11使用的规范,它属于该规范的附件F:

本附录规定了对IEC 60559浮点标准的C语言支持。 IEC 60559浮点标准特别适用于微处理器系统的二进制浮点算法,第二版(IEC 60559:1989),以前称为IEC 559:1989和IEEE二进制浮点运算标准(ANSI / IEEE 754-1985) )。用于与基数无关的浮点运算的IEEE标准(ANSI / IEEE854-1987)推广了二进制标准,以消除对基数和字长的依赖性。 IEC 60559通常指浮点标准,如IEC 60559操作,IEC 60559格式等。


11
投票

在新的[concepts.equality]中,在略有不同的背景下,我们有:

如果给定相等的输入,表达式产生相等的输出,则表达式是等式保持的。表达式的输入是表达式操作数的集合。表达式的输出是表达式的结果以及表达式修改的所有操作数。

并非所有输入值都必须对给定表达式有效;例如,对于整数ab,当a / bb时,表达式0没有明确定义。这并不排除表达式a / b是保持相等的。表达式的域是一组输入值,需要对表达式进行明确定义。

虽然这个表达式域的概念并未在整个标准中完全表达,但这是唯一合理的意图:语法要求是类型的属性,语义要求是实际值的属性。

更一般地说,我们也有[structure.requirements]/8

本文档中定义的任何概念的必要操作不必是总函数;也就是说,所需操作的某些参数可能导致无法满足所需的语义。 [示例:在<s上运行时,StrictTotallyOrdered概念所需的NaN运算符([concept.stricttotallyordered])不符合该概念的语义要求。 - 结束示例]这不影响类型是否满足概念。

这特别指的是概念,而不是像Cpp17LessThanComparable这样的命名要求,但这是理解库如何工作的正确精神。


当Cpp17LessThanComparable给出语义要求时

<是一种严格的弱有序关系(24.7)

违反此规定的唯一方法是提供一对违反严格弱排序要求的值。对于像double这样的类型,那将是NaNmin(1.0, NaN)是未定义的行为 - 我们违反了算法的语义要求。但对于没有NaN的浮点数,<是一个严格的弱排序 - 所以这很好......你可以使用minmaxsort,你喜欢什么。

展望未来,当我们开始编写使用operator<=>的算法时,域的概念是表达ConvertibleTo<decltype(x <=> y), weak_ordering>的句法要求的一个原因是错误的要求。有x <=> ypartial_ordering是好的,它只是看到x <=> ypartial_ordering::unordered不是的一对值(至少我们可以通过[[ assert: (x <=> y) != partial_ordering::unordered ]];诊断)


3
投票

唯一可能的(不仅仅是合理的)解释是方程适用于函数范围内的值;这是算法中实际使用的值。

您可能会想到一个定义一组值的类型,但对于UDT来说无论如何都没有意义。你对范围的解释是一种类型的每一种可能的价值都是荒谬的。

这不是问题。

这可能在实现中存在一个非常严重的问题,其中浮点值的精度不能超过类型允许的精度,因为浮点类型的数学值的整个概念失去了所有含义,因为编译器可能决定更改浮点类型的值以随时删除精度。实际上,在这种情况下不能定义语义。任何此类实现都被破坏,任何程序可能只是偶然的。

编辑:

类型没有为算法定义一组值。对于具有未在任何代码中正式指定的内部不变量的用户数据类型,这是显而易见的。

可在任何容器中使用的值集合,算法(容器内部使用元素上的算法)......是该容器或算法的特定使用的属性。这些库组件没有共享它们的元素:如果你有两个set<fraction> S1和S2,它们的元素将不会被另一个使用:S1将比较S1中的元素,S2将比较S2中的元素。这两组存在于不同的“宇宙”中,它们的逻辑属性是孤立的。不变量是独立存在的;如果你在S2中插入一个不小于或大于S1中x1的元素x2(因此被认为是等价的),你不希望在S1中x1的位置找到x2!容器之间不可能共享数据结构,并且不能在算法之间共享元素(这些算法不能具有模板类型的静态变量,因为它具有意外的生命周期)。

有时标准是一个谜语,你必须找到正确的解释(最合理,最有用,最有可能是有意的);如果委员会成员被要求澄清一个问题,他们将解决最X的解释(X =似是而非,有用......),即使它与之前的确切措辞相矛盾,所以当文本模糊不清或给出疯狂的结论时,你不妨跳过文字阅读并跳转到最有用的。

这里唯一的解决方案是模板化库组件的每次使用都是独立的,并且方程式只需在使用期间保持。

您不希望vector<int*>无效,因为指针可能具有无法复制的无效值:仅使用此类值是非法的。

从而

vector<int*> v;
v.push_back(new int);
vector<int*> v2 = v; // content must be valid
delete v[0];
v[0] = null; // during v[0] invocation (int*)(v[0]) has no valid value

是有效的,因为元素类型的必需属性在需要它们的小持续时间内有效。

在这种情况下,我们可以调用向量的成员函数,因为它知道其元素不遵循Assignable概念,因为没有允许赋值,因为无异常保证不允许它:存储在v[0]中的值不能被v[0]vector<>::operator[]中允许的元素没有用户定义的操作。

库组件只能使用特定函数描述中提到的特定操作对该调用中使用的值;即使对于内置类型,它也不能以任何其他方式创建值:如果在特定实例中未插入或查找0,则特定的set<int,comp>实例可能无法将值与0进行比较,因为0甚至可能不在comp的域中。

所以内置或类类型在这里统一处理。即使用内置类型实例化,库实现也不能对值集合采取任何内容。

© www.soinside.com 2019 - 2024. All rights reserved.