В чем разница между точкой последовательности и приоритетом оператора?
Рассмотрим пример классической последовательности:
i = i++;
В стандартах на C и С++ указано, что поведение вышеуказанного выражения undefined, поскольку оператор = не связан с точкой последовательности.
Меня смущает то, что ++
имеет более высокий приоритет, чем =
, и поэтому приведенное выше выражение на основе приоритета должно сначала оценить i++
, а затем выполнить назначение. Таким образом, если мы начинаем с i = 0
, мы всегда должны заканчивать с i = 0
(или i = 1
, если выражение было i = ++i
), а не undefined. Что мне не хватает?
Ответы
Ответ 1
Все операторы производят результат. Кроме того, некоторые операторы, такие как оператор присваивания =
и составные операторы присваивания (+=
, ++
, >>=
и т.д.), Создают побочные эффекты. Различие между результатами и побочными эффектами лежит в основе этого вопроса.
Приоритет оператора определяет порядок, в котором операторы применяются для получения своих результатов. Например, правила приоритета требуют, чтобы *
проходил до +
, +
до &
и т.д.
Однако приоритет оператора ничего не говорит о применении побочных эффектов. Здесь вступают в действие последовательности (секвентированные до, секвенированные после и т.д.). Они говорят, что для того, чтобы выражение было четко определено, приложение побочных эффектов в одно и то же место в памяти должно быть разделено точкой последовательности.
Это правило разбивается на i = i++
, потому что оба ++
и =
применяют свои побочные эффекты к одной и той же переменной i
. Во-первых, ++
идет, потому что имеет более высокий приоритет. Он вычисляет свое значение, беря i
исходное значение до приращения. Тогда =
идет, потому что имеет более низкий приоритет. Его результатом является также исходное значение i
.
Ключевая вещь, которая здесь отсутствует, - это точки последовательности, разделяющие побочные эффекты двух операторов. Это то, что делает поведение undefined.
Ответ 2
Приоритет операторов (и ассоциативность) определяет порядок, в котором выражение анализируется и выполняется. Однако это ничего не говорит о порядке оценки операндов, что является другим термином. Пример:
a() + b() * c()
Приоритет оператора указывает, что результат b()
и результат c()
должны быть умножены перед добавлением вместе с результатом a()
.
Однако он ничего не говорит о порядке, в котором эти функции должны выполняться. Этот порядок определяет порядок оценки каждого оператора. Чаще всего порядок оценки неуточнен (неопределенное поведение), что означает, что стандарт позволяет компилятору делать это в любом порядке, который ему нравится. Компилятор не должен документировать этот порядок и не должен вести себя последовательно. Причиной этого является предоставление компиляторам большей свободы в синтаксическом анализе выражения, что означает более быструю компиляцию и, возможно, более быстрый код.
В приведенном выше примере я написал простую тестовую программу, и мой компилятор выполнил вышеуказанные функции в порядке a()
, b()
, c()
. Тот факт, что программа должна выполнять как b()
, так и c()
, прежде чем она сможет умножить результаты, не означает, что она должна оценивать эти операнды в любом заданном порядке.
Здесь присутствуют точки последовательности. Это заданная точка в программе, где должны выполняться все предыдущие оценки (и операции). Таким образом, точки последовательности в основном связаны с порядком оценки и не столько с приоритетом оператора.
В приведенном выше примере три операнда не имеют последовательности по отношению друг к другу, что означает, что никакая точка последовательности не определяет порядок оценки.
Поэтому становится проблематичным, когда побочные эффекты вводятся в таких неестественных выражениях. Если мы напишем i++ + i++ * i++
, то мы до сих пор не знаем порядка, в котором эти операнды оцениваются, поэтому мы не можем определить, каким будет результат. Это связано с тем, что как +
, так и *
имеют неуказанный/неточный порядок оценки.
Если бы мы написали i++ || i++ && i++
, то поведение было бы корректно определено, поскольку &&
и ||
определяет порядок оценки слева направо и есть точка последовательности между оценкой левый и правый операнды. Таким образом, if(i++ || i++ && i++)
является совершенно портативным и безопасным (хотя и нечитаемым) кодом.
Что касается выражения i = i++;
, проблема здесь в том, что =
определяется как (6.5.16):
Побочный эффект обновления сохраненного значения левого операнда секвенируется после вычисления значений левого и правого операндов. Оценки операндов не подвержены влиянию.
Это выражение на самом деле близко к тому, чтобы быть четко определенным, потому что в тексте фактически говорится, что левый операнд не должен обновляться до того, как вычисляется правый операнд. Проблема заключается в самом последнем предложении: порядок оценки операндов неуточнен/нелогичен.
И поскольку выражение содержит побочный эффект i++
, он вызывает поведение undefined, так как мы не можем знать, сначала ли оценивается операнд i
или операнд i++
.
(Там больше, так как в стандарте также говорится, что операнд не должен использоваться дважды в выражении для несвязанных целей, но это другая история.)
Ответ 3
Приоритет операторов и порядок оценки - это две разные вещи. Давайте взглянем на них один за другим:
Правило приоритета оператора: В операндах выражений, более жестких к операторам с более высоким приоритетом.
Например
int a = 5;
int b = 10;
int c = 2;
int d;
d = a + b * c;
В выражении a + b * c
приоритет *
выше, чем у +
, и поэтому b
и c
будут привязаны к *
, и выражение будет проанализировано как a + (b * c)
.
Порядок оценки:. Он описывает, как операнды будут вычисляться в выражении. В заявлении
d = a>5 ? a : ++a;
a
гарантированно оценивается перед оценкой ++b
или c
.
Но для выражения a + (b * c)
, хотя *
имеет более высокий приоритет, чем +
, не гарантируется, что a
будет оцениваться до или после b
или c
и даже не b
и c
, заказанных для их оценки. Даже a
, b
и c
могут оцениваться в любом порядке.
Простым правилом является то, что приоритет оператора не зависит от порядка оценки и наоборот.
В выражении i = i++
более высокий приоритет ++
просто указывает компилятору связать i
с оператором ++
и что он. Он ничего не говорит о порядке оценки операндов или о том, какой побочный эффект (один оператор =
или один на ++
) должен иметь место первым. Компилятор может делать что угодно.
Переименуем i
слева от присваивания be il
и справа от присваивания (в выражении i++
) будет ir
, тогда выражение будет выглядеть как
il = ir++ // Note that suffix l and r are used for the sake of clarity.
// Both il and ir represents the same object.
Теперь компилятор может свободно оценивать выражение il = ir++
либо как
temp = ir; // i = 0
ir = ir + 1; // i = 1 side effect by ++ before assignment
il = temp; // i = 0 result is 0
или
temp = ir; // i = 0
il = temp; // i = 0 side effect by assignment before ++
ir = ir + 1; // i = 1 result is 1
что приводит к двум различным результатам 0
и 1
, которые зависят от последовательности побочных эффектов при назначении и ++
и, следовательно, вызывает UB.