Рассмотрим практический пример, когда отсутствие оператора return в функции, возвращающей значение, приводит к неопределённому поведению. Отличная демонстрация того, как ломается неправильный код, который благодаря везению мог ранее успешно работать многие годы.
Мы рассматриваем паттерн ошибки, который стандарт кодирования SEI CERT C++ описывает так: MSC52-CPP. Value-returning functions must return a value from all exit paths.
The C++ Standard, [stmt.return], paragraph 2 [ISO/IEC 14882-2014], states the following:
Flowing off the end of a function is equivalent to a return with no value; this results in undefined behavior in a value-returning function.
Простой пример кода с ошибкой:
int foo(T a, T b)
{
if (a < b)
return -1;
else if (a > b)
return 1;
}
Программист забыл написать return 0, если два значения равны. Не все ветви выполнения возвращают значение, и это приводит к неопределённому поведению программы.
В общем-то думаю, всё понятно. Достаточно известный и распространённый паттерн ошибки. Проверяя с помощью анализатора PVS-Studio открытые проекты, мы регулярно находим благодаря диагностике V591 ошибки этого типа: примеры.
Если всё понятно и ошибки ищутся, то зачем, собственно, эта заметка? Вот тут мы подошли к самому интересному!
Дело в том, что программисты постоянно интерпретируют неопределённое поведение более узко, чем оно есть на самом деле. Неопределённое поведение при забытом операторе return часто воспринимается так: функция вернёт любое случайное значение. Более того, предыдущий опыт программиста может подтверждать, что это именно так.
Нет! Неправильно! Неопределённое — значит неопределённое. Нельзя предположить, что будет. Код, который работал одним образом, может начать работать по-иному.
Чтобы это продемонстрировать, я приведу немного отредактированную дискуссию (RU) с сайта RSDN.
Название темы: забавный крэш
Linux, libc-2.33, GCC 11.1.0, оптимизация -O2, следующий код падает с SIGSEGV:
#include <string>
#include <iostream>
bool foobar(const std::string &s)
{
std::string sx = s;
std::cout << sx << std::endl;
}
int main(int argc, char **argv)
{
foobar(argv[0]);
return 0;
}
/home/user/test$ g++ -O2 -std=c++11 ./test.cpp -o ./test && ./test
./test.cpp: In function 'bool foobar(const string&)':
./test.cpp:8:1: warning: no return statement in function returning non-void [-Wreturn-type]
8 | }
| ^
./test
Segmentation fault (core dumped)
А если поменять bool foobar на void foobar или добавить return false, то не падает.
Не падает и при использовании старого компилятора – GCC 7.5.0.
Кстати, std::string, как выяснилось, никак не влияет на ситуацию. Аналог этого кода на C также падает, будучи собранным g++.
#include <stdio.h>
bool foobar(const char *s)
{
printf("foobar(%s)\n", s);
}
int main(int argc, char **argv)
{
foobar(argv[0]);
return 0;
}
/home/user/test$ g++ -O2 ./test.c -o ./test && ./test
./test.c: In function 'int foobar(const char*)':
./test.c:6:1: warning: no return statement in function returning non-void [-Wreturn-type]
6 | }
| ^
foobar(./test)
Segmentation fault (core dumped)
Если так: gcc -O2 ./test.c -o ./test && ./test, то всё хорошо.
Компилятор просто не генерирует инструкцию возврата из функции (ret)!
0000000000001150 <_Z6foobarPKc>:
1150: 48 89 fe mov rsi,rdi
1153: 48 83 ec 08 sub rsp,0x8
1157: 48 8d 3d a6 0e 00 00 lea rdi,[rip+0xea6] # 2004 <_IO_stdin_used+0x4>
115e: 31 c0 xor eax,eax
1160: e8 cb fe ff ff call 1030 <printf@plt>
1165: 66 2e 0f 1f 84 00 00 00 00 00 cs nop WORD PTR [rax+rax*1+0x0]
116f: 90 nop
0000000000001170 <__libc_csu_init>:
1170: f3 0f 1e fa endbr64
1174: 41 57 push r15
Спасибо пользователю ononim с сайта RSDN за такой интересный пример.
Вот такое яркое непривычное проявление неопределенного поведения.
Какие выводы можно сделать из этого? На мой взгляд их два:
Дополнительные ссылки: