我在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 ++。据我所知,由于缓存数组是常量的,这意味着它可以是只读的,只需要初始化一次作为静态,它会一次又一次地初始化,因为这个数组的值是每个返回的值。逗号分隔的递归函数调用,它违反了声明的数组的行为。因此,它给出了未定义的行为,这也在规则中说明。
对此更好的解释是什么?
答案 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 ++,而是在如何实现该语言方面。