Очень частный вопрос, который задавали мне, задавал я сам себе и другим. Да и каждый C++ разработчик, к сожалению, должен его задавать.
Ответ на него в общем случае — никак. Это алгоритмически неразрешимая задача практически ничем не отличающаяся от задачи останова. Но программистов как палками ни гоняй, все равно будут решать неразрешимые задачи, так что для конкретного кода и для конкретных входных данных иногда есть способы дать ответ.
Можно проверить код до компиляции различными статическими анализаторами:
- CppCheck
- Clang Static Analyzer
- PVS Studio
- И другие
Достаточно умный анализатор, работающий с графом потока выполнения программы, знающий сотни ловушек стандарта, способен найти многие проблемы и привлечь внимание к сомнительному коду. Но не все и не всегда.
Компиляторы Clang
и GCC
с включенными флагами -Wall -Wpedantic
способны находить некоторые ошибки.
Мы можем сами проверять часть кода в compile-time на различных наборах входных данных, используя constexpr
. В контексте, вычисляемом на этапе компиляции, UB запрещено:
constexpr int my_div(int a, int b) {
return a / b;
}
namespace test {
template <unsigned int N>
constexpr int div_test(const int (&A)[N], const int (&B)[N]) {
int x = 0;
for (auto i = 0u; i < N; ++i) {
x = ::my_div(A[i], B[i]);
}
return x;
}
constexpr int A[] = {1,2,3,4,5};
constexpr int B[] = {1,2,3,4,0};
static_assert((div_test(A, A), true)); // OK
static_assert((div_test(A, B), true)); // Compliation error, zero division
}
Но constexpr
не везде применим, в зависимости от версии стандарта налагает ограничения на тело функции, а также неявно применяет inline
спецификатор, «запрещая» отрывать определение функции в отдельную единицу трансляции (или, по-простому, определение придется разместить в заголовочном файле).
Наконец, если мы не смогли найти ошибки статическим анализом (внешними утилитами или компилятором), можно прибегнуть в динамическому анализу.
При сборке компиляторами Clang
или GCC
можно включить санитайзеры
-fsanitize=undefined
, -fsanitize=address
, -fsanitize=thread
, позволяющие отлавливать ошибки в run-time, но ценой значительных накладных расходов, так что пользоваться этими средствами нужно только на этапе тестирования и разработки.
Также для отладочных сборок код стандартных библиотек иногда инструментирован assert'aми. Так, например, сделано для различных итераторов стандартной библиотеке в поставке msvc
(Visual Studio)
Поскольку неопределенное поведение проявляется в возможностях оптимизации тем или иным компилятором, нужно собирать свой код под разные платформы, с разными уровнями оптимизаций, и сравнивать его поведение. Код без ошибок должен быть переносимым и вести себя одинаково (если, конечно, его задача не генерировать совершенно случайные значения).
Тесты, различные сборки, статический и динамический анализ — способы немного поднять уверенность в том, что в вашем коде нет UB. Дать же точную гарантию может только коллегия экспертов, которые будут сверять каждую строчку кода с буквой стандарта и трижды друг друга перепроверять. И даже этого может быть недостаточно.
Еще есть путь отключения каких-либо оптимизаций флагами компилятора, флаги включающие различные нарушения стандарта (знаменитый -fpermissive
), превращающие язык C++ во что-то совершенно иное. Но призываю вас никогда не идти этим путем. Ваш код станет непереносимым. Ваш код перестанет быть кодом на C++. Лучше сразу возьмите другой язык программирования.
- https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html
- https://clang.llvm.org/docs/AddressSanitizer.html
- https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html
- https://shafik.github.io/c++/undefined_behavior/2019/05/11/explporing_undefined_behavior_using_constexpr.html
- https://docs.microsoft.com/en-us/cpp/build/reference/permissive-standards-conformance?view=msvc-160
- http://cppcheck.sourceforge.net/