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

时间:2015-10-28 17:33:30

标签: 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;
}

根据来源给出错误:

terminate called after throwing an instance of '__gnu_cxx::recursive_init_error'
  what():  std::exception

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

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

对此更好的解释是什么?

2 个答案:

答案 0 :(得分: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);
  }    
} 

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

答案 1 :(得分:6)

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

  

如果控制在变量为的时候递归地重新输入声明   在被初始化时,行为是不确定的。

以下提供了有用的示例:

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

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

重要的是要回忆一下未定义的行为意味着什么。未定义的行为意味着一切都可能发生,而“一切”自然会包含抛出的异常。

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