【问题标题】:Does this program with bounded recursion have undefined behavior?这个有界递归的程序是否有未定义的行为?
【发布时间】:2019-06-28 17:50:24
【问题描述】:

我知道无限递归或迭代是未定义的行为,但有界不是。但是,对于大多数输入,该程序段都会出错。它是否有未定义的行为,为什么或为什么不?如果它具有未定义的行为,是否可以进行一些修改以消除未定义的行为?

#include <cstdint>

using bigint = ::std::uint64_t;
constexpr const bigint add_val = 1442695040888963407;
constexpr const bigint mult_val = 6364136223846793005;

bigint compute(bigint t)
{
   if (t > 1) {
      return add_val + compute(t * mult_val);
   } else {
      return 1;
   }
}

int main(int argc, char const * const argv[])
{
   return compute(argc < 0 ? -argc : argc);
}

这基本上是使用递归循环遍历 linear congruential random number generator with a period of 2^64 的所有值,因此它保证最终会达到 0 或 1。

我在问一个非常明确和具体的问题。这个任何人都可以编译和运行的程序是否会根据 C++ 标准调用未定义的行为?

【问题讨论】:

  • 如果传入的t大于1,会一直这样直到溢出,不是吗?
  • 好吧,根据我完全没有受过教育的估计,在参数的值是终止递归的仅有的两个可能值之一之前,我预计大约有 2^63 次递归调用(平均而言)。那么,你有足够的堆栈空间来进行 2^63 次递归调用吗?
  • @SamVarshavchik - 嗯,这取决于我在哪台计算机上运行它,不是吗?我无法访问任何具有足够堆栈空间的计算机。而且我认为现在这个星球上的其他任何人都不会这样做。但这不是我的问题。
  • 看起来t 在几乎所有情况下都大于 1。所以会被调用直到溢出
  • 我想问题是“标准是否对最大调用堆栈大小施加了限制?”。我怀疑它没有,堆栈溢出是技术上必要的不合规问题。但我不确定。编辑:我能找到的唯一提到的“溢出”是关于整数的,唯一提到的“堆栈”是关于 std::stack 和堆栈展开。编辑 2:“递归”的唯一提及是在 “允许递归调用,main 函数除外。”.

标签: c++ recursion c++17 undefined-behavior


【解决方案1】:

感谢101010的标准链接,我认为相关的短语是

4.1 实施合规性 [intro.compliance]

(2.1) - 如果程序不违反本文档中的规则,则符合要求的实现应在其资源限制内接受并正确执行该程序。

附件 B(资料性)

实施数量 [implimits]

主要解决编译器限制(最大符号长度、每行字符等),但语言听起来相关:

  1. 由于计算机是有限的,C++ 实现不可避免地受限于它们可以成功处理的程序的大小。 每个实施都应记录已知的限制。本文档可能会引用存在的固定限制,说明如何根据可用资源计算可变限制,或者说固定限制不存在或未知。

  2. 这些限制可能会限制数量,包括下述数量或其他数量......

因此,该标准没有说明哪些数量可能会受到实施的限制,也没有给出除了它所描述的那些限制的最小值的指南之外的任何其他内容。

如果你的编译器没有记录它的堆栈深度限制,我猜它可能不符合 bold 句子,但是声明“堆栈深度受这些限制运行时属性” 可能就足够了..

这个任何人都可以编译和运行的程序是否会根据 C++ 标准调用未定义的行为?

没有。但根据 4.1/2.1 实现,允许无法编译或无法正确执行正确的程序。

【讨论】:

  • 这个答案正是我一直在寻找的东西。谢谢!
  • 我确实有一个小问题。最大堆栈大小不是对运行实现的数据处理系统的限制,而不是实现限制吗?
  • 该标准是否会禁止在没有优化的情况下资源限制不足以运行任何程序但能够“优化”一组模糊定义的程序的实现,以便它们能跑吗?
  • 该标准只能提供一种判断实现是否合规的方法,它不能禁止任何东西。无论如何,我怀疑这样的实现将难以吸引客户,无论在技术上是否符合要求。
  • @Omnifarious,实际上有多个限制:给定寻址模式下的最大可能堆栈大小,编译器支持的最大值(如果不同),运行时系统限制允许的最大值,运行时用户限制的最大值,并且 max 在给定时刻实际可访问。 AFAIK 它们都按标准归类为“实施”。
【解决方案2】:

事实第一,来自标准草案N4800 §7.1/P4 [expr.pre](Emphasis Mine):

如果在计算表达式期间,结果未在数学上定义或不在可表示的值范围内 它的类型,行为是未定义的。 [注意:除法的处理 零,使用零除数形成余数,所有浮点数 例外情况因机器而异,有时可通过 库函数。 ——尾注]

还有条款 §6.7.1/p2 Fundamental types [basic.fundamental](Emphasis Mine):

对于每个标准的有符号整数类型,都存在一个 对应的(但不同的)标准无符号整数类型: “无符号字符”、“无符号短整数”、“无符号整数”、“无符号长整数” int”和“unsigned long long int”。同样,对于每个扩展 有符号整数类型,存在对应的扩展无符号 整数类型。标准和扩展的无符号整数类型是 统称为无符号整数类型。无符号整数类型 具有与相应的有符号整数相同的范围指数 N 类型。 无符号类型的可表示值范围为0 至 2^N - 1(含);执行无符号类型的算术运算 模 2^N 。 [注意:无符号算术不会溢出。溢出 对于有符号算术产生未定义的行为(7.1)。 — 尾注]

还有 §5.13.2/p2 整数文字 [lex.icon]

整数字面量的类型是对应列表的第一个 在表7中可以表示其值。

还有 §7.4 常用算术转换 [expr.arith.conv]

(1.5) — 否则,应执行积分促销 (7.3.6) 在两个操作数60 上。那么以下规则应适用于 提升的操作数:

  • (1.5.1) — 如果两个操作数的类型相同,则无需进一步转换。

  • (1.5.2) — 否则,如果两个操作数都具有有符号整数类型或都具有无符号整数类型,则具有较小类型的操作数 整数转换等级应转换为操作数的类型 排名更高。

  • (1.5.3) — 否则,如果具有无符号整数类型的操作数的等级大于或等于另一个类型的等级 操作数,带符号整数类型的操作数应转换为 无符号整数类型的操作数的类型。

  • (1.5.4) — 否则,如果带符号整数类型的操作数的类型可以表示带符号的操作数类型的所有值 无符号整数类型,无符号整数类型的操作数应为 转换为带符号整数类型的操作数的类型。

  • (1.5.5) — 否则,两个操作数都应转换为对应于操作数类型的无符号整数类型 有符号整数类型

所以问题是:这个程序中的算术结果是否在数学上定义并在其类型的可表示值范围内。更具体地说,表达式1442695040888963407 + compute(t * 6364136223846793005) 是否在其类型的可表示值范围内?

为此,整数文字 1442695040888963407 和 6364136223846793005 的类型必须小于或等于 conversion rankstd::uint64_t,以便结果转换为 std::uint64_t。不幸的是,没有这样的保证。

因此,为了让您的程序避免 UB,我会用 LU 标记整数文字。

bigint compute(bigint t)
{
   if (t > 1) {
      return 1442695040888963407LU + compute(t * 6364136223846793005LU);
   } else {
      return 1;
   }
}

现在,至于为什么会出现分段错误,是由于堆栈溢出。虽然,上面的程序理论上没有无限的递归,也就是UB,但是递归的次数会耗尽你机器的资源。

【讨论】:

  • 是否有任何理由相信任何实际系统都会存在整数类型,这会导致指示的常量提升到比uint64_t 更高级别的类型?尝试适应这种可能性是否有任何有用的目的?
  • 我将修改我的程序,使这个答案不再适用。 :-) 我的问题是关于即使递归肯定是有界的,出于所有实际目的的程序的行为是否会发生堆栈溢出。而且我意识到它将在所有当前机器上发生堆栈溢出。问题是,程序是否调用了未定义的行为。 :-) 这是一个非常好的答案。 :-)
【解决方案3】:

该标准的标准作者认识到,不同的实现可以而且应该在他们可以有效处理的程序范围上有所不同。虽然标准可能已经规定质量实施应该尝试有效地处理尽可能广泛的任务——“实用性”的定义留给实施者的判断——但他们可能认为这样的概念是自我的——很明显。

如果术语“未定义的行为”适用于标准不会对其施加任何要求的每个程序,那么所有程序都会调用未定义的行为,除了一个警告:符合要求的实现必须能够处理至少一个——可能人为的和无用的——以标准定义的方式编程。因此,如果一个程序不执行标准描述为未定义行为的任​​何操作,则标准将定义该程序在由符合标准的实现运行时的行为,该实现无法以标准定义的方式运行任何其他程序。标准。它不会强加任何程序在任何其他情况下的行为。

【讨论】:

    猜你喜欢
    • 2023-03-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-06-13
    • 2013-04-25
    • 1970-01-01
    相关资源
    最近更新 更多