【问题标题】:Is the behaviour of a program that has undefined behaviour on an unreachable path defined? [duplicate]在无法到达的路径上具有未定义行为的程序的行为是否已定义? [复制]
【发布时间】:2018-06-20 23:01:13
【问题描述】:

考虑

void swap(int* a, int* b)
{
    if (a != b){
        *a = *a ^ *b;
        *b = *a ^ *b;
        *a = *a ^ *b;
    }   
}

int main()
{
    int a = 0;
    int b = 1;
    swap(&a, &b); // after this b is 0 and a is 1
    return a > b ? 0 : a / b;
}

swap 试图欺骗编译器使其不优化程序。

是否定义了该程序的行为? a / b 永远无法访问,但如果是,那么您将得到除以零。

【问题讨论】:

  • 我对 UB 的理解是,它需要一条不可避免地导致包含 UB 的表达式的路径。也就是说,只要有可能没有达到带有 UB 的表达式,就没有 UB。虽然我找不到来源。否则,在调用成员函数之前检查nullptr 等常见策略将是UB。
  • @FrançoisAndrieux 我会说 UB 是当且仅当采用表达式的路径时。
  • @You:在 C++ 标签上。我在 C 标签上问过这个问题,因为 C 中的规则通常更简单。
  • @You 那不一样。这个问题是在问,而是将 UB 引入分支足以迫使编译器假设该分支是不可访问的。
  • @You 我认为 C 可能与 C++ 具有相同的 UB 定义,也可能不同,这是答案的一部分。 C 和 C++ 不同的语言,据我了解,随着时间的推移,它们的差异越来越大。没有人说它们必须具有相同的行为,所以我认为作为 C++ 问题的副本结束是不合适的。

标签: c language-lawyer undefined-behavior


【解决方案1】:

没有必要基于任何给定代码结构或实践的有用性,也没有必要基于任何关于 C++ 的内容,无论是在其标准中还是在另一个 SO 答案中,无论 C++ 的定义有多么相似.要考虑的关键是C's definition of undefined behavior

行为,使用不可移植或错误的程序构造或 错误数据,本国际标准对此没有规定 要求

(C2011,3.4.3/1;强调)

因此,未定义的行为是临时触发的(“在使用”构造或数据时),而不仅仅是存在。* 这对于数据引起的未定义行为是一致的,并且源于程序结构;那里的标准不必是一致的。正如另一个答案所描述的,这种“使用时”定义是一个很好的设计选择,因为它允许程序避免执行与错误数据相关的未定义行为。

另一方面,如果一个程序确实执行了未定义的行为,那么根据标准的定义,程序的整个行为都是未定义的。这种随之而来的不确定性是一种更普遍的类型,因为与错误数据或构造直接关联的 UB 原则上可能包括改变程序其他部分的行为,甚至是追溯性的(或显然如此)。当然,可能发生的事情有语言外的限制 - 所以不,鼻恶魔实际上不会出现 - 但这些并不一定像人们想象的那样强大。


* 警告:某些程序结构在翻译时使用。这些在程序翻译中产生 UB,导致程序的每次执行都具有完全未定义的行为。举一个有点愚蠢的例子,如果您的程序源代码没有以未转义的换行符结尾,那么程序的行为是完全未定义的(参见C2011, 5.1.1.2/1,第 2 点)。

【讨论】:

  • 你的引用不能说服我。使用不可移植的程序构造可能是开发人员将其添加到程序中,因此使用了该构造,即使它从未执行过。 use 这个词不清楚。标准中有词汇表吗?
  • 这是一个很好的答案,不仅涵盖了根据标准对未定义行为的严格定义,还涵盖了解释其含义和后果所涉及的细微差别。
  • @FilipHaglund:除了引用之外,如果您考虑一下,替代方案完全是荒谬的,至少在给定的除以零的示例中;如果仅存在未定义行为的构造会导致未定义行为,即使在不可访问时,您根本不能使用除法运算符 - 如果不可访问性不会阻止未定义行为,则任何检查零的尝试都将没有实际意义。
  • @Aleksi 好点,现在我相信了 :)
【解决方案2】:

未计算的表达式的行为与程序的行为无关。如果对表达式求值,则未定义的行为与程序的行为无关。

如果是这样,那么这段代码将毫无用处:

if (p != NULL)
    …; // Use pointer p.

(您的 XOR 可能具有未定义的行为,因为它们可能会产生陷阱表示。您可以通过将对象声明为易失性来破坏此类学术示例的优化。如果对象是易失性的,则 C 实现无法知道其值是否可能会因外部手段而改变,因此每次使用对象都需要实现读取其值。)

【讨论】:

  • 请注意,如果可以证明最终会到达,则到达带有 UB 的表达式并不是绝对必要的。
  • @FrançoisAndrieux 不清楚这意味着什么。 C 程序通常是确定性的。如果在特定运行中可以到达路径(通过确定的输入/时间和/或rand 输出),则到达。
  • @EugeneSh。我的意思是,如果可以证明一个路径在所有条件下都通向带有 UB 的表达式,则该程序具有 UB。也就是说,如果在给定点可以证明所有路径都通向 UB,则该程序在该点已经是 UB。编译器可以做出决定并采取相应的行动,在达到实际的 UB 表达式之前改变行为。
  • @Bathsheba:如果您认为可以改善问题,请继续编辑问题。如果需要匹配,请编辑一些答案。我可以稍后复习。现在得走了。
  • @ruakh 在这种情况下,UB 表达式并非不可访问错误报告分支不会返回或以其他方式阻止控制继续通过该函数,因此假定控制最终会到达 UB。相反,该示例是说可以删除分支,因为在优化期间,因为唯一可以采用它的情况也是肯定会达到 UB 的情况。
【解决方案3】:

一般来说,如果执行会调用未定义行为的代码,如果不执行,则一定不会产生任何影响。然而,在少数情况下,现实世界的实现可能会以相反的方式表现并拒绝生成虽然不违反约束但不可能以定义的行为执行的代码。

extern struct foo z;

int main(int argc, char **argv)
{
    if (argc > 2) z;
    return 0;
}

根据我对标准的阅读,它明确将不完整类型上的左值转换描述为调用未定义行为(除其他外,尚不清楚什么实现可以为此类事物生成代码),因此标准不会对行为施加任何要求如果argc 是 3 或更多。但是,如果argc 为 2 或更少,我无法确定标准中上述代码会违反的任何约束,也没有任何理由不应该完全定义行为。尽管如此,包括 gcc 和 clang 在内的许多编译器都完全拒绝上述代码。

【讨论】:

  • if (argc > 2) z;,编译器应该计算z,但是z是一个不完整类型的变量,所以它不能被计算。编译器拒绝代码是正确的。\
  • @JonathanLeffler:标准将不完整类型的评估描述为 UB 而不是违反约束。恕我直言,它应该做的是有一个构造类别,编译器可以在闲暇时接受或拒绝,而不考虑它们是否被执行,但不得干扰操作,除非(1)它们导致程序被拒绝,或(2 ) 他们被执行。
  • 好的;因此编译器正确地决定他们将拒绝您调用 UB 的尝试。因为他们不知道你的真正意思是什么,当它是 UB 时他们可以做他们喜欢做的事。我不认为这是编译器问题。这是糟糕的 C 代码。
  • @JonathanLeffler:我在标准中看不到任何东西表明如果使用argc <= 2 调用上述程序将没有定义的行为。虽然我同意标准可能不应该暗示质量编译器必须接受这样的程序,但我能看到的实现的唯一理由是声明这样的程序超出了某些实现限制--一个可以证明用几乎任何程序做几乎任何事情的漏洞。
  • 我们最好在此停止讨论。如果您愿意,我们可以删除所有 cmets。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-04-23
  • 1970-01-01
  • 1970-01-01
  • 2017-11-28
  • 1970-01-01
  • 2022-07-19
相关资源
最近更新 更多