Код С++ с поведением undefined, компилятор генерирует std:: exception
В С++ я нашел интересное безопасное правило кодирования, в котором говорится:
Не запускать функцию во время инициализации объявления статической переменной. Если функция повторно вводится во время постоянной инициализации статического объекта внутри этой функции, поведение программы undefined. Бесконечная рекурсия не требуется для запуска поведения undefined, функция должна возвращаться только один раз как часть инициализации.
Несоответствующий пример такой же:
#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).
Я сомневаюсь, что мое понимание правильное в отношении этого понятия, поскольку я не хорошо разбираюсь в С++. Согласно мне, поскольку массив кеша является постоянным, что означает, что он может быть доступен только для чтения и должен быть инициализирован только один раз как статический, он получает инициализацию снова и снова, поскольку значения для этого массива - это значение, возвращаемое каждым из рекурсивные функции, разделенные запятыми, которые противоречат поведению объявленного массива. Таким образом, он дает поведение undefined, которое также указано в правиле.
Что лучше для этого?
Ответы
Ответ 1
Чтобы выполнить 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[]
будет инициализироваться только один раз, но его инициализация больше не зависит от самого себя. Поэтому у нас нет проблем.
Ответ 2
В стандарте С++, §6.7/4, говорится следующее об инициализации переменных области блока со статической продолжительностью хранения:
Если управление повторно вводит декларацию рекурсивно, в то время как переменная при инициализации поведение undefined.
Приводится следующий информативный пример:
int foo(int i) {
static int s = foo(2*i); // recursive call - undefined
return i+1;
}
Это относится и к вашему примеру. fact(0)
- это рекурсивный вызов, поэтому декларация cache
вводится повторно. Undefined вызывается.
Важно вспомнить, что означает поведение Undefined. Undefined поведение означает, что все может случиться, а "все" вполне естественно включает исключения, которые бросаются.
Undefined поведение также означает, что вы больше не можете рассуждать о чем-либо еще в коде, за исключением случаев, когда вы действительно хотите перейти к деталям реализации компилятора. Но тогда вы больше не говорите о С++ с точки зрения использования языка программирования, а в том, как реализовать этот язык.