Вопрос в том, ввели ли мы неопределенное поведение, которое отключает оптимизатор, или мы можем подать отчет об ошибке в gcc?
Извините за отсутствие лучшего заголовка, но оно довольно хрупкое, и мыпочти уверены, что это ошибка.Минимальный пример - не наш любимый дизайн, но он основан на сбойном коде:
#include <iostream>
struct Node
{
Node(Node* parent) {
if(parent) {
parent->child_ = this;
}
}
Node* child()
{
return child_;
}
Node* child_ = nullptr;
};
void walk(Node* module, int cleanup) {
if(module != nullptr) {
if(!cleanup) {
std::cerr << "No cleanup";
}
walk(module->child(), cleanup);
if(cleanup) {
delete module;
}
}
}
int main (){
Node* top = new Node(nullptr);
Node* child = new Node(top);
walk(top,1);
}
Скомпилировано с -O1 -foptimize-sibling-calls -ftree-vrp
.Пример Godbolt: https://gcc.godbolt.org/z/4VijKb
Программа аварийно завершает работу, вызывая module->child()
, когда модуль 0x0
.Осматривая ассемблер, мы заметили, что if (module != nullptr)
пропускается в начале walk
.Существует проверка для cleanup
, и вызов work
кажется безусловным, что приводит к попытке извлечь child_
из недопустимого указателя.
Проверка восстанавливается в сборке (и кажется, что кодработа), если:
- Любая из двух оптимизаций по
-O1
убрана. - Тело
if(!cleanup)
удалено.(Никаких побочных эффектов от cerr
) - Тело
if(cleanup)
удалено.(Утечка памяти, но я думаю, что это считается наблюдаемым изменением поведения) walk
вызывается до "Нет очистки" if
.(Порядок работы) Тип cleanup
изменен на bool
с int
.(Изменение типа - но, как мне кажется, не наблюдается заметного изменения поведения). - Вставка безусловных
cerr << "text";
перед и if(!cleanup)
.(Также наблюдаемое изменение.)
Кажется странной комбинацией хвостовой рекурсии и nullptr
удаления проверки, которая привела к неправильному коду.Возможно, walk
был разделен на функции-братья и сестры на основе cleanup
проверок и неправильно прошит (?).
Два кандидата на UB были:
- Компоновщик хинтов, который
module
не-nullptr
, но я не вижу, как компилятор мог бы вывести результат. - Использование
int
в bool
контексте, но это допустимо AFAIK.
FWIW clang
, кажется, выдает правильное время выполнения, gcc 8.3
также имеет сборку для проверки.9.1
и trunk
нет.У нас нет эксперта по gcc, поэтому мы не знали, почему оптимизатор может быть введен в заблуждение.