此具有受限递归的程序是否具有未定义的行为?

时间:2019-06-28 17:50:24

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

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

#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 ++标准调用未定义的行为?

3 个答案:

答案 0 :(得分:3)

借助101010的标准链接,我认为相关短语是

  

4.1实施合规性[intro.compliance]

     

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

  

附件B(仅供参考)

     

实施数量[限制]

主要解决了编译器的局限性(最大符号长度,每行字符等),但是语言听起来很相关:

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

  2.   
  3. 限制可能会限制包括以下描述的数量或其他...

  4.   

因此,该标准没有说明实施可能会限制哪些数量,或者没有提供有关其所描述的那些限制的最小值的准则。

如果您的编译器未记录其堆栈深度限制,那么根据 bold 语句,我猜它可能不符合要求,但声明“堆栈深度受这些限制运行时属性” 可能就足够了。

  

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

不。但是,按照4.1 / 2.1的实现,允许编译失败或无法正确执行正确的程序。

答案 1 :(得分:2)

事实首先来自标准草案N4800§7.1/ P4 [expr.pre](重点矿):

  

如果在对表达式求值时,结果在数学上未定义或不在可表示值的范围内    [注意:除法处理   零,使用零除数和所有浮点数形成余数   机器之间的例外情况有所不同,有时可以通过   库功能。 —尾注]

还有第6.7.1 / p2节基本类型[basic.fundamental](重点矿):

  

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

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

  

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

enter image description here

还有§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,但递归数却耗尽了计算机的资源。

答案 2 :(得分:0)

该标准的作者认识到,不同的实现可能并且应该在它们可以有效处理的程序范围上有所不同。尽管标准可能已经规定,高质量的实现应该尝试有效地处理尽可能多的实际任务-实施者可以自行决定“实用性”的定义-但他们可能认为这样的想法将是自我的。明显。

如果将“未定义行为”一词应用于标准不对其施加任何要求的每个程序,则除一个警告外,所有程序都将调用未定义行为:合格的实现必须能够处理至少一个(可能)人为的和无用的-以标准定义的方式编写程序。因此,如果某个程序不会执行该标准表征为“未定义行为”的任何动作,则该标准将定义由合格实现运行的程序的行为,而该实现不能以程序定义的方式运行任何其他程序。标准。在任何其他情况下,它都不会对任何程序的行为施加任何行为。