# V567. Modification of variable is unsequenced relative to another operation on the same variable. This may lead to undefined behavior.

Анализатор обнаружил выражение, которое приводит к неопределённому поведению программы. Переменная неоднократно используется между двумя точками следования, при этом её значение изменяется. В результате невозможно предсказать результат работы такого выражения. Рассмотрим понятия "неопределённое поведение" и "точка следования" более подробно.

Неопределённое поведение (англ. 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++);`.
- В C++ перегруженные операторы выступают в роли функций, поэтому точкой следования является вызов перегруженного оператора.

Рассмотрим теперь несколько примеров, приводящих к неопределённому поведению:

    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)`. Порядок вычисления аргументов функции зависит от компилятора и настроек оптимизации.

Дополнительные ресурсы

- Википедия. Неопределённое поведение.
- Википедия. Точка следования.
- Елена Сагалаева. Точки следования (sequence points).
- Klaus Kreft & Angelika Langer. Sequence Points and Expression Evaluation in C++.
- Дискуссия на сайте bytes.com. Sequence points.
- Дискуссия на сайте StackOverflow.com. Why is a = (a+b) - (b=a) a bad choice for swapping two integers?
- cppreference.com. Order of evaluation

Данная диагностика классифицируется как:

CWE-758 CERT-EXP30-C

Взгляните на примеры ошибок, обнаруженных с помощью диагностики V567.