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

时间:2019-03-14 00:39:09

标签: c++ floating-point language-lawyer undefined-behavior c++-standard-library

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

简短阅读

(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) == true,但equiv(0.0, 1.0) == false我们得出结论,< 不是严格的弱排序。因此,(IEEE 754)double 不是 Cpp17LessThanComparable ,这违反了std::min Requires 子句,并且std::max

最后,15.5.4.11/1说:

  

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

更新1:

问题的重点不是争辩说std::min(0.0, 1.0)是未定义的,并且当程序评估该表达式时,可能发生任何事情。它返回0.0。期。 (我从来没有怀疑过。)

重点是要显示标准的(可能)缺陷。在对精度的赞扬中,标准经常使用数学术语,而严格的严格排序只是一个例子。在这种情况下,数学精度和推理必须始终如一。

例如,查看Wikipedia对strict weak ordering的定义。它包含四个要点,每个要点都以“对于S ...中的每个x ...”开头。他们都没有说“对于S中的x值对算法有意义”(什么算法?)。另外,std::min的说明很清楚,即“ T应该是 Cpp17LessThanComparable ”,这意味着<是{{1 }}。因此,T在Wikipedia的页面中扮演集合S的角色,并且当完整考虑T的值时,必须包含四个项目符号。

很明显,NaN是与其他double值完全不同的野兽,但它们是 still 可能的值。我没有在标准中看到任何内容(很大,共1719页,因此这个问题和语言律师标签)从数学上得出的结论T可以满足在不涉及NaN的情况下加倍。

实际上,人们可以说NaN很好,而其他双精度是问题!确实,回想一下,存在多个可能的NaN double值(其中2 ^ 52-1,每个值承载不同的有效负载)。考虑包含所有这些值和一个“正常”双倍数(例如42.0)的集合S。在符号中,S = {42.0,NaN_1,...,NaN_n}。事实证明std::min是对S的严格弱排序(证据留给读者)。 C ++委员会在将<指定为“请不要使用任何其他值,否则严格的弱排序会被破坏并且std::min的行为未定义”时,是否想到过这套值?我敢打赌不是,但我宁愿在《标准》中阅读这篇文章,也不要ulating测“某些值”的含义。

更新2:

std::min(上面)的声明与std::min 24.7.9的声明进行对比:

  

clamp
  要求:template<class T> constexpr const T& clamp(const T& v, const T& lo, const T& hi);的值不得大于lo。对于第一种形式,键入   T应为 Cpp17LessThanComparable (表24)。   [...]
  [注意:如果避免使用hi,则T可以是浮点类型。 —尾注]

在这里,我们清楚地看到“ NaN可以加倍使用,前提是不涉及NaN。”我正在为std::clamp寻找相同类型的句子。

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

  

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

这显然是解决我在此处提出的问题的尝试,但是是在概念的背景下进行的(正如Barry指出的, Cpp17LessThanComparable 不是一个概念)。另外,恕我直言,此段也缺乏准确性。

3 个答案:

答案 0 :(得分:11)

在新的[concepts.equality]中,在稍微不同的情况下,我们有:

  

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

     

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

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

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

  

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

这是专门针对概念的,而不是诸如 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的一对值不是(至少我们可以通过{{1}来诊断})

答案 1 :(得分:6)

免责声明:我不知道完整的C ++标准,我确实研究了有关float的内容。我确实知道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

因为我意识到下面的阅读部分可能很无聊,所以这是一个比较示例中NaN风险的玩具示例(我什至不尝试排序算法……):

#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

这意味着与您的假设相反,当涉及到NaN甚至是std::min时, -0.0是不对称的。 NaN不会传播。简短的故事:这确实使我对以前的项目感到有些痛苦,在该项目中,我必须实现自己的min函数以按照项目规范的要求在两侧正确传播NaN。由于{strong上的std::min未定义!

IEEE 754:

您已经注意到,IEEE 754浮点数(或ISO / IEC / IEEE 60559:2011-06,这是C11标准使用的规范,请参阅下文,该副本或多或少地复制了C语言的IEEE754)没有严格的弱排序,因为NaN违反了不可比性的传递性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:在这种情况下,可能会出现未定义的行为   文档省略了行为的任何明确定义或当程序   使用错误的构造或错误的数据。允许未定义   行为范围包括完全忽略情况   不可预测的结果,在翻译或编程过程中表现   以书面形式执行环境特征   (无论是否发出诊断消息),直至终止   翻译或执行(带有诊断信息   信息)。许多错误的程序构造不会导致未定义   行为;他们需要被诊断。常数评估   表达式永远不会表现出明确指定为undefined的行为   (7.7)。 —尾注]

没有所谓的“屈服”未定义行为。这只是C ++标准中未定义的内容。这可能意味着您可能会使用它并自负风险(如果这样做std::min(0.0, 1.0);会得到正确的结果),或者如果您发现确实对浮点数非常谨慎的编译器,则可能会引发警告甚至编译错误! / p>

关于子集……您说:

  

我在标准书中看不到任何内容(该书很大,共有1719页,   因此,这个问题和语言律师标签)   从数学上得出以下结论:std :: min与   在不涉及NaN的情况下加倍。

我自己也没有阅读过该标准,但是从您发布的部分来看,该标准似乎已经说得很好。我的意思是,如果您构造新的类型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)

  

关系和相等运算符支持通常的数学运算   数值之间的关系。对于任何有序数字对   恰好珍惜其中一种关系-更少,更大和相等-   是真的。关系运算符可能会提高“无效”浮点数   参数值为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格式等。

答案 2 :(得分:3)

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

您可能会想到定义一组值的类型,但是对于UDT而言,这毫无意义。您对范围就是类型的每个可能值的解释显然是荒谬的。

这没问题这里

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

编辑:

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

在任何容器,算法中可用的值集(容器内部在元素上使用算法)...是该容器或算法的特定用途的属性。这些库组件没有共享它们的元素:如果您有两个set<fraction> S1和S2,则它们的元素将不会被其他组件使用:S1将比较S1中的元素,S2将比较S2中的元素。这两个集合存在于不同的“宇宙”中,并且它们的逻辑属性是孤立的。不变量对于每个独立成立。 如果您在S2中插入不小于或大于S1中x1的元素x2(因此被认为是等效的),您就不会期望在S1中x2的位置找到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

之所以有效,是因为元素类型的必需属性在需要它们的短时间内有效。

在这种情况下,我们可以调用向量的成员函数,知道它的元素不遵守可分配概念,因为不允许分配,因为无例外保证不允许它:{{中存储的值v[0]不能使用1}},v[0]允许的元素上没有用户定义的操作。

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

因此,内置或类类型在此处统一处理。即使使用内置类型实例化,库实现也不能在值集上假设任何内容。