Точки последовательности в printf

Я прочитал здесь, что есть точка последовательности:

После действия, связанного с спецификатором формата преобразования ввода/вывода. Например, в выражении printf("foo %n %d", &a, 42) имеется точка последовательности после вычисления %n перед печатью 42.

Однако, когда я запускаю этот код:

int your_function(int a, int b) {
    return a - b;
}

int main(void) {
    int i = 10;

    printf("%d - %d - %d\n", i, your_function(++i, ++i), i);
}

Вместо того, чего я ожидаю, я получаю:

12 - 0 - 12

Это означает, что не было точки последовательности, созданной для спецификатора формата преобразования. Является ли http://en.wikipedia.org неправильным или просто неправильно понял что-то или gcc несовместим в этом случае (кстати, Visual Studio 2015 дает тот же неожиданный результат)?

EDIT:

Я понимаю, что порядок аргументов your_function оценивается и присваивается параметрам undefined. Я не спрашиваю о том, почему мой средний срок равен 0. Я спрашиваю, почему остальные два условия равны 12.

Ответ 1

Поскольку этот вопрос задавался из-за обсуждения на основе комментариев here, я предоставлю некоторый контекст:

первый комментарий: порядок операций не гарантирован быть порядком, в котором вы передаете аргументы функции. Некоторые люди (ошибочно) предполагают, что аргументы будут оцениваться справа налево, но в соответствии со стандартом поведение undefined.

OP принимает и понимает это. Нет смысла повторять тот факт, что your_function(++i, ++i) является UB.

В ответ на этот комментарий: благодаря вашему комментарию я вижу, что printf может быть оценен в любом порядке, но я понял, что это потому, что аргументы printf являются частью va_list. Вы говорите, что аргументы любой функции выполняются в произвольном порядке?

OP просит уточнить, поэтому я немного уточнил:

Второй комментарий: Да, это именно то, что я говорю. даже вызов int your_function(int a, int b) { return a - b; } не гарантирует, что передаваемые вами выражения будут оцениваться слева направо. Нет точки последовательности (точка, в которой выполняются все побочные эффекты предыдущих оценок). Возьмите этот пример. Вложенный вызов является точкой последовательности, поэтому внешний вызов проходит i+1 (13), а возвращаемое значение внутреннего вызова (undefined, в этом случае -1, потому что i++, i оценивается до 12, 13, очевидно), , но нет гарантии, что это всегда будет иметь место

Это сделало довольно ясным, что эти типы конструкций запускают UB для всех функций.


Беспокойство Википедии

OP Цитирует это:

После действия, связанного с спецификатором формата преобразования ввода/вывода. Например, в выражении printf ( "foo% n% d", & a, 42) существует точка последовательности после вычисления% n перед печатью 42.

Затем применяет его к своему фрагменту (prinf("%d - %d - %d\n", i, your_function(++i, ++i), i);), преследуя спецификаторы формата, чтобы служить точками последовательности.
То, о чем говорится в описании "спецификатор формата ввода/вывода", является спецификатором %n. Соответствующий аргумент должен быть указателем на целое число без знака, и ему будет присвоено количество символов, напечатанных до сих пор. Естественно, %n должен быть оценен до того, как остальные аргументы будут напечатаны. Однако использование указателя, переданного для %n в других аргументах, по-прежнему опасно: он не UB (ну, это не так, но это может быть):

printf("Foo %n %*s\n", &a, 100-a, "Bar");//DANGER!!

Перед вызовом функции есть точка последовательности, поэтому выражение 100-a будет оценено до того, как %n установит &a на правильное значение. Если a неинициализирован, то 100-a - UB. Если a инициализируется равным 0, например, результат выражения будет равен 100. В целом, однако, этот вид кода в значительной степени требует неприятностей. Рассматривайте это как очень плохую практику, или хуже...
Просто посмотрите на результат, сгенерированный одним из следующих операторов:

unsigned int a = 90;
printf("%u %n %*s\n",a,  &a, 10, "Bar");//90         Bar
printf("%u\n", a);//3
printf("Foo %u %n %*s\n",a, &a, 10-a, "Bar");//Foo 3      Bar < padding used: 10 - 3, not 10 - 6 
printf("%u\n", a);//6

Как видите, n переназначается внутри printf, поэтому вы не можете использовать его новое значение в списке аргументов (потому что есть точка последовательности). Если вы ожидаете, что n будет переназначен "на месте", вы, по сути, ожидаете, что C выскочит из вызова функции, оценит другие аргументы и вернется обратно в вызов. Это просто невозможно. Если вы должны были изменить unsigned int a = 90; на unsigned int a;, то поведение undefined.


Что касается 12 '

Теперь, когда OP считывает точки последовательности, он правильно замечает, что это утверждение:

printf("%d - %d - %d\n", i, your_function(++i, ++i), i);

Немного отличается: your_function(++i, ++i) есть точка последовательности, а гарантирует, что i будет увеличиваться дважды. Этот вызов функции является точкой последовательности, потому что:

Прежде чем функция будет введена в вызов функции. Порядок, в котором оцениваются аргументы, не указан, но эта точка последовательности означает, что все их побочные эффекты завершены до ввода функции

Это означает, что перед вызовом printf your_function должен быть вызван (поскольку его возвращаемое значение является одним из аргументов для вызова printf) и i будет увеличиваться в два раза. Этот мог объяснять, что результат "12 - 0 - 12", но гарантированно ли это выход?

Нет

Технически, хотя большинство компиляторов сначала оценит вызов your_function(++i, ++i);, стандарт позволит компилятору оценить аргументы, переданные в sprintf слева направо (порядок не указывается после всех). Таким образом, это будет равноправный результат:

10 - 0 - 12
//or even
12 - 0 - 10
//and
10 - 0 - 10
//technically, even this would be valid
12 - 0 - 11

Хотя последний выход крайне маловероятен (это было бы очень неэффективно)

Ответ 2

Я думаю, вы неправильно поняли текст о точках последовательности printf (SP). Они как-то аномалии и только с %n, потому что этот спецификатор формата имеет побочные эффекты, и эти побочные эффекты нужно упорядочить.

Во всяком случае, есть SP в начале выполнения printf() и после оценки всех аргументов. Эти SP-спецификаторы формата все после этого, поэтому они не влияют на вашу проблему.

В вашем примере использование i все в аргументах функции, и ни один из них не разделен точками последовательности. Поскольку вы изменяете значение (дважды) и используете значение без промежуточных точек последовательности, ваш код UB.

Что такое правило о SP в printf означает, что этот код хорошо сформирован:

int x;
printf("%d %n %d %n\n", 1, &x, 2, &x);

хотя значение x изменяется дважды.

Но этот код UB:

int x = 1;
printf("%d %d\n", x, ++x);

ПРИМЕЧАНИЕ. Помните, что %n означает, что количество написанных до сих пор символов копируется в целое число, указанное указанным аргументом.

Ответ 3

Приняв четкий ответ на этот вопрос, строго выполняется (даже предотвращается) правилами С по порядку оценки и UB.

Указанные правила порядка оценки указаны здесь:

C99 раздел 6.7.9, p23: 23 Оценки списка инициализации выражения неопределенно упорядочены по отношению друг к другу и, следовательно, порядок, в котором происходят какие-либо побочные эффекты, неуточнен.

И этот вызов функции будет демонстрировать поведение undefined:

your_function(++i, ++i)

Из-за UB, в сочетании с правилами порядка оценки, точные прогнозы ожидаемых результатов для следующего:

printf("%d - %d - %d\n", i, your_function(++i, ++i), i);

невозможны.

Edit
... Я не спрашиваю, почему мой средний срок равен 0. Я спрашиваю, почему остальные два условия равны.

Нет гарантии, какой из трех аргументов вышеуказанной функции вызывается первым. (из-за правил С по порядку оценки). И если первая функция сначала оценивается, то в этот момент вы вызывали Undefined Behavior. Кто может действительно сказать, почему два других условия - 12?. Потому что, что происходит с i, когда оценивается второй аргумент, кто-то догадывается.