Анализатор обнаружил выражение, которое приводит к неопределённому поведению программы. Переменная неоднократно используется между двумя точками следования, при этом её значение изменяется. В результате невозможно предсказать результат работы такого выражения. Рассмотрим понятия "неопределённое поведение" и "точка следования" более подробно.
Неопределённое поведение (англ. undefined behavior) — свойство некоторых языков программирования (наиболее заметно в C и C++) в определённых ситуациях выдавать результат, зависящий от реализации компилятора. Другими словами, спецификация не определяет поведение языка в любых возможных ситуациях, а говорит: "при условии А результат операции Б не определён". Допускать такую ситуацию в программе считается ошибкой, даже если на некотором компиляторе программа успешно выполняется, она не будет кроссплатформенной и может отказать на другой машине, в другой ОС и даже при других настройках компилятора.
Точка следования (англ. sequence point) — в императивном программировании любая точка программы, в которой гарантируется, что все побочные эффекты предыдущих вычислений уже проявились, а побочные эффекты последующих еще отсутствуют.
Их часто упоминают, говоря о C и C++, поскольку в этих языках особенно просто записать выражение, значение которого может зависеть от неопределённого порядка проявления побочных эффектов. Добавление одной или нескольких точек следования задаёт порядок более жестко и является одним из методов достижения устойчивого (т.е. корректного) результата.
Стоит заметить, что в C++11 вместо точек следования ввели понятия sequenced before/after, sequenced и unsequenced. Многие выражения, приводящие к неопределённому поведению в C++03, стали определены (например, i = ++i
). Эти правила также дополнялись в C++14 и C++17. Анализатор выдаёт срабатывание независимо от используемого стандарта. Определённость выражений вида i = ++i
не служит оправданием к их использованию. Такие выражения лучше переписать, сделав их более понятными коллегам. Также, если потребуется поддержать более ранний стандарт, можно получить трудно отлаживаемый баг.
Примеры неопределённого поведения в зависимости от стандартов:
i = ++i + 2; // undefined behavior until C++11 i = i++ + 2; // undefined behavior until C++17 f(i = -2, i = -2); // undefined behavior until C++17 f(++i, ++i); // undefined behavior until C++17, // unspecified after C++17 i = ++i + i++; // undefined behavior cout << i << i++; // undefined behavior until C++17 a[i] = i++; // undefined behavior until C++17 n = ++i + i; // undefined behavior
Точки следования необходимы в ситуации, когда одна и та же переменная изменяется в выражении более одного раза. Часто в качестве примера приводят выражение i=i++
, в котором происходит присваивание переменной i
и её же инкремент. Какое значение примет i
? Стандарт языка должен либо указать одно из возможных поведений программы как единственно допустимое, либо указать диапазон допустимых поведений, либо указать, что поведение программы в данном случае совершенно не определено. В языках C и C++ вычисление выражения i=i++
приводит к неопределённому поведению, поскольку это выражение не содержит внутри себя ни одной точки следования.
В C и C++ определены следующие точки следования:
&&
(логическом И), ||
(логическом ИЛИ) и операторах-запятых. Например, в выражении *p++ != 0 && *q++ != 0
все побочные эффекты левого операнда *p++ != 0
проявятся до начала каких-либо действий в правом.a = (*p++) ? (*p++) : 0
точка находится после первого операнда *p++
. При вычислении второго выражения, переменная p
уже увеличена на 1.a=b;
), выражения в инструкциях return
, управляющие выражения в круглых скобках инструкций ветвления if
или switch
и циклов while
или do-while
и все три выражения в круглых скобках цикла for
.f(i++) + g(j++) + h(k++)
каждая из трёх переменных: i
, j
и k
, принимает новое значение перед входом в f
, g
и h
соответственно. Однако порядок вызова функций f()
, g()
, h()
не определён, следовательно, не определён и порядок инкремента i
, j
, k
. Значения j
и k
в теле функции f оказываются неопределенными. Следует отметить, что вызов функции нескольких аргументов f(a,b,c)
не является случаем применения оператора-запятой и не определяет порядок вычисления значений аргументов.(1+i++) в int a = (1+i++);
.Рассмотрим теперь несколько примеров, приводящих к неопределённому поведению:
int i, j; ... X[i]=++i; X[i++] = i; j = i + X[++i]; i = 6 + i++ + 2000; j = i++ + ++i; i = ++i + ++i;
Во всех этих случаях невозможно предсказать результат вычислений. Конечно, эти примеры искусственны и опасность в них видна сразу. Рассмотрим пример кода, взятого из реального приложения:
while (!(m_pBitArray[m_nCurrentBitIndex >> 5] & Powers_of_Two_Reversed[m_nCurrentBitIndex++ & 31])) {} return (m_nCurrentBitIndex - BitInitial - 1);
Компилятор может вычислить вначале как левый, так и правый аргумент оператора &
. Это значит, что переменная m_nCurrentBitIndex
может быть уже увеличена на единицу при вычислении m_pBitArray[m_nCurrentBitIndex >> 5]
. А может быть ещё и не увеличена.
Этот код может долго и исправно работать. Однако следует учитывать, что гарантированно корректно он будет себя вести только при сборке определённой версией компилятора с неизменным набором параметров компиляции. Корректный вариант кода:
while (!(m_pBitArray[m_nCurrentBitIndex >> 5] & Powers_of_Two_Reversed[m_nCurrentBitIndex & 31])) { ++m_nCurrentBitIndex; } return (m_nCurrentBitIndex - BitInitial);
Этот код более не содержит неоднозначностей. Заодно исчезла магическая константа -1
.
Программисты часто считают, что неопределённое поведение может возникать только при использовании постинкремента, в то время как преинкремент безопасен. Это не так. Рассмотрим пример общения на эту тему.
Вопрос:
Скачал ознакомительную версию вашей студии, прогнал свой проект и получил такое предупреждение: V567 Undefined behavior. The 'i_acc' variable is modified while being used twice between sequence points.
Код
i_acc = (++i_acc) % N_acc;
Как мне кажется, здесь нет undefined behavior, так как переменная i_acc
не участвует в выражении дважды.
Ответ:
Неопределённое поведение здесь есть, хотя вероятность проявления ошибки весьма мала. Оператор =
не является точкой следования. Это значит, что сначала компилятор может поместить значение переменной i_acc
в регистр, затем увеличить значение в регистре, вычислить выражение и записать результат в переменную i_acc
, после чего вновь записать в эту переменную регистр с увеличенным значением. В результате мы получим код вида:
REG = i_acc; REG++; i_acc = (REG) % N_acc; i_acc = REG;
Компилятор имеет на это полное право. Конечно, на практике, скорее всего он сразу увеличит значение переменной и тогда всё посчитается так, как ожидает программист. Но полагаться на это нельзя.
Рассмотрим ещё одну ситуацию, связанную с вызовом функций.
Порядок вычисления аргументов функции не определён. Если аргументами является изменяющаяся переменная, то результат будет непредсказуем. Это неуточнённое поведение. Рассмотрим пример:
int A = 0; Foo(A = 2, A);
Функция Foo
может быть вызвана как с аргументами (2, 0)
, так и с аргументами (2, 2)
. Порядок вычисления аргументов функции зависит от компилятора и настроек оптимизации.
Дополнительные ресурсы
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V567. |