【问题标题】:Do std::min(0.0, 1.0) and std::max(0.0, 1.0) yield undefined behavior?std::min(0.0, 1.0) 和 std::max(0.0, 1.0) 会产生未定义的行为吗?
【发布时间】:2019-08-04 19:09:39
【问题描述】:

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

短读

(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)doublenotCpp17LessThanComparable 违反了std::minstd::maxRequires 子句。

最后,15.5.4.11/1 说:

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

更新 1:

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

重点是显示标准的(可能的)缺陷。在对精确度的值得称赞的追求中,该标准经常使用数学术语,弱严格排序只是一个例子。在这些情况下,数学精度和推理必须一路走好。

例如,看看维基百科对strict weak ordering 的定义。它包含四个要点,每个要点都以“For every x [...] in S...”开头。他们都没有说“对于 S 中对算法有意义的某些值 x”(什么算法?)。此外,std::min 的规范明确表示“T 应为Cpp17LessThanComparable”,这意味着<T 上的严格弱排序。因此,T 在 Wikipedia 页面中扮演集合 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 可以是浮点类型。 ——尾注]

在这里,我们清楚地看到“std::clamp 可以在不涉及 NaN 的情况下使用双精度数”。我正在为std::min 寻找相同类型的句子。

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

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

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

【问题讨论】:

  • 当行为未定义时,是因为可能的运行时值。一些函数/语言特性有一个狭窄的合同(例如,不能取消引用nullptr)。而在这些情况下,程序员有责任排除这些情况。由于 UB 不能在 constexpr 上下文中发生,我尝试将 std::min 放入带有 1.0/0 一个参数的 static_assert 中,但它没有编译,因为我无法在编译时生成 NaN。我认为如果可以在编译时检测到违反要求,它应该只是使编译失败。无论如何,措辞是不幸的。
  • “重复”并没有说明有问题的代码是否是UB
  • 发现以下论文讨论了该主题及其对排序等事物的影响:Comparison in C++
  • 不幸的是,这个问题对 IEEE 浮点数的关注似乎适得其反,因为它与实际问题并不真正相关,但却吸收了很多词。可以很容易地做到something like this(这显然不是严格的弱排序,不需要谈论 NaN 或引用其他标准来确定这一点)。

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


【解决方案1】:

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

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

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

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

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

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

这专门指概念,而不是像 Cpp17LessThanComparable 这样的命名需求,但这是理解库的工作原理的正确精神。


Cpp17LessThanComparable给出的语义要求是

&lt; 是严格的弱排序关系 (24.7)

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

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

【讨论】:

  • +1 这个答案很好地捕捉了标准的意图和方向(还要注意新的段落 [structure.requirements]/8)。但是,就今天的情况(C++17)而言,我觉得 DainsDwarf 的回答更正确。 (当然,我可能错了。)关于你在他们的帖子中作为评论发布的问题,我最初的想法是“这很明显”。鉴于我们在这里进行的整个讨论,我不再认为这很明显。我能说的最好的就是“我不知道,但我相信如此”,这不是很好。我所知道的是,在这件事上的精确度正变得越来越重要。
  • 抱歉:在我进行更新 2 之前,我没有意识到您提到了 [structure.requirements]/8。我对其进行了编辑以在应得的地方给予赞扬。
【解决方案2】:

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

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

短读:

标准没有说std::min(0.0, 1.0); 是未定义的行为,它说constexpr const double&amp; min(const double&amp; a, const double&amp; 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;
}

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

$ 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。因为双打上的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:可能会出现未定义的行为当这 文档省略了任何明确的行为定义或当程序 使用错误的构造或错误的数据。允许的未定义 行为范围从完全无视情况 不可预知的结果,翻译或程序中的行为 以环境特征的记录方式执行 (无论是否发出诊断消息),终止 翻译或执行(与诊断 信息)。许多错误的程序结构不会产生未定义 行为;他们需要被诊断出来。常数的评估 表达式从不表现出明确指定为未定义的行为 在本文件 (7.7) 的第 4 条至第 14 条中。 ——尾注]

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

关于子集……你说:

我在标准中看不到任何内容(相当大,1719 页, 因此这个问题和语言律师标签) 从数学上得出结论 std::min 可以 如果不涉及 NaN,则为双打。

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

我们还可以查看double 上的操作&lt; 的标准定义,该定义在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 格式等。

【讨论】:

  • 这个答案对我来说很有意义。几点。 1)我承认我的语言不准确,我知道在这种情况下 UB 意味着 std::min 的声明是未定义的(而不是 std::min(0.0, 1.0) 的表达式。但我必须用这个标题说这个问题更吸引人:-)
  • "标准没有说std::min(0.0, 1.0); 是未定义的行为,它说constexpr const double&amp; min(const double&amp; a, const double&amp; b); 是未定义的行为。" - 这是一个有趣的主张,对我来说是全新的。你有什么要备份的吗?
  • @DainDwarf float 没有总订单并不是真正的问题(这确实是问题的不幸焦点,因为它似乎也不是超级相关) - 我的问题更多的是关于关于实例化本身是UB的具体断言,而不是它的具体使用。
  • 有许多函数在其所有输入的整个域上都没有定义(或表现良好)。但某些部分没有被定义并不意味着它们在其他地方没有被定义和有用!
  • @Deduplicator:我认为这个答案声称使用不符合 UB 中标准指定要求的类型来实例化 std::min。它可能违反标准,但在任何特定的库实现中都不会有 UB。我认为std::min(a,b) 的真正定义必须完全等同于(a&lt;b) ? a : b。这对 NaN 具有明确定义的行为:如果任一输入为 NaN,则比较为假,因此它返回第二个操作数。 (有趣的事实:这种行为exactly matches x86's minsd instruction
【解决方案3】:

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

您可能会想到定义一组值的类型,但对于 UDT 而言,这无论如何都没有意义。您将范围解释为类型的每个可能值显然是荒谬的。

这没问题这里

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

编辑:

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

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

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

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

您不会期望 vector&lt;int*&gt; 无效,因为指针可能具有无法复制的无效值:只有使用这样的值是非法的。

因此

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 概念,因为不允许赋值,因为无异常保证不允许它:存储在 @987654324 中的值@ 不能被v[0] 使用,vector&lt;&gt;::operator[] 中允许的元素上没有用户定义的操作。

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

所以内置或类类型在这里被统一处理。即使使用内置类型实例化,库实现也不能对一组值进行任何假设。

【讨论】:

  • 这可能是唯一可能的解释,但似乎我们必须更改/添加一些词以使其与标准的措辞兼容。此处可能需要进行编辑更改。
  • “您将范围解释为类型的每个可能值显然是荒谬的。”我发现恰恰相反,即考虑到“算法中实际使用的值”的集合是“显然是荒谬的”。那些价值观是什么?严格弱排序的定义没有提到任何算法。请查看更新。
  • 意图是一回事,规范是另一回事。 OP是关于后者的,你的帖子是关于前者的。我毫不怀疑意图。当然,规范是由意图驱动的,但它可能会产生意想不到的后果。
  • @CassioNeri:没有任何版本的 C 或 C++ 标准始终如一地采用使其成为完整规范所需的精度水平。相反,它们都依赖实现来使用常识来填补各种空白。不过,这里讨论的问题很棘手,因为人们可能会争论 double 是否应该适合需要 Cpp17LessThanComparable 的模板,还是应该强制编译器尝试不同的模板。
  • @supercat 确实,我不这么说。但我很清楚,C++ 委员会试图尽可能精确,而且他们还依赖用户对措辞的反馈。他们甚至有适当的程序让任何人报告他们可能在标准中发现的任何问题(包括措辞)。 “完美是无法实现的,但如果我们追求完美,我们就能捕捉到卓越。” (文斯·隆巴迪)。
猜你喜欢
  • 2023-03-15
  • 2017-12-18
  • 2013-10-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-11-20
  • 1970-01-01
  • 2022-07-08
相关资源
最近更新 更多