具有未定义行为的C++代码,编译器生成std :: exception

POO*_*PTA 14 c++ arrays static

我在C++中遇到了一个有趣的安全编码规则:

在初始化静态变量声明期间不要重新输入函数.如果在该函数内的静态对象的常量初始化期间重新输入函数,则程序的行为是未定义的.触发未定义的行为不需要无限递归,该函数只需要在初始化时重复一次.

不合规的例子是:

#include <stdexcept>

int fact(int i) noexcept(false) {
  if (i < 0) {
    // Negative factorials are undefined.
    throw std::domain_error("i must be >= 0");
  }

  static const int cache[] = {
    fact(0), fact(1), fact(2), fact(3), fact(4), fact(5),
    fact(6), fact(7), fact(8), fact(9), fact(10), fact(11),
    fact(12), fact(13), fact(14), fact(15), fact(16)
  };

  if (i < (sizeof(cache) / sizeof(int))) {
    return cache[i];
  }

  return i > 0 ? i * fact(i - 1) : 1;
}
Run Code Online (Sandbox Code Playgroud)

根据来源给出错误:

terminate called after throwing an instance of '__gnu_cxx::recursive_init_error'
  what():  std::exception
Run Code Online (Sandbox Code Playgroud)

Visual Studio 2013中执行时.我尝试了类似的自己的代码并得到了相同的错误(使用g ++编译并在Ubuntu上执行).

如果我的理解对于这个概念是正确的,我很怀疑,因为我不熟悉C++.据我所知,由于缓存数组是常量的,这意味着它可以是只读的,只需要初始化一次作为静态,它会一次又一次地初始化,因为这个数组的值是每个返回的值.逗号分隔的递归函数调用,它违反了声明的数组的行为.因此,它给出了未定义的行为,这也在规则中说明.

对此更好的解释是什么?

Bar*_*rry 17

要执行fact(),您需要先静态初始化fact::cache[].最初fact::cache,您需要执行fact().那里有一个循环依赖,这导致你看到的行为.cache只会被初始化一次,但它需要自己初始化才能初始化自己.即使输入这个也让我头晕目眩.

引入这样的缓存表的正确方法是将其分成不同的函数:

int fact(int i) noexcept(false) {
  if (i < 0) {
    // Negative factorials are undefined.
    throw std::domain_error("i must be >= 0");
  }

  return i > 0 ? i * fact(i - 1) : 1;
} 

int memo_fact(int i) noexcept(false) {
  static const int cache[] = {
    fact(0), fact(1), fact(2), fact(3), fact(4), fact(5),
    fact(6), fact(7), fact(8), fact(9), fact(10), fact(11),
    fact(12), fact(13), fact(14), fact(15), fact(16)
  };

  if (i < (sizeof(cache) / sizeof(int))) {
    return cache[i];
  }
  else {
    return fact(i);
  }    
} 
Run Code Online (Sandbox Code Playgroud)

这里memo_fact::cache[]只会初始化一次 - 但它的初始化不再依赖于它自己.所以我们没有问题.


Chr*_*ckl 6

C++标准§6.7/ 4说明了关于具有静态存储持续时间的块范围变量的初始化:

如果控件在初始化变量时以递归方式重新输入声明,则行为未定义.

以下信息示例如下:

int foo(int i) {
static int s = foo(2*i); // recursive call - undefined
return i+1;
}
Run Code Online (Sandbox Code Playgroud)

这也适用于您的示例.fact(0)是递归调用,因此cache重新输入声明.调用未定义的行为.

重要的是回忆一下未定义的行为意味着什么.未定义的行为意味着一切都可能发生,而"一切"很自然地包括被抛出的异常.

未定义的行为还意味着您不能再对代码中的任何其他内容进行推理,除非您真的想要了解编译器实现的详细信息.但是,在使用编程语言方面,你不再谈论C++,而是在如何实现该语言方面.