Порядок оценки параметров перед вызовом функции в C

Можно ли предположить порядок оценки параметров функции при ее вызове в C? Согласно следующей программе, когда я ее выполнял, кажется, что нет определенного приказа.

#include <stdio.h>

int main()
{
   int a[] = {1, 2, 3};
   int * pa; 

   pa = &a[0];
   printf("a[0] = %d\ta[1] = %d\ta[2] = %d\n",*(pa), *(pa++),*(++pa));
   /* Result: a[0] = 3  a[1] = 2    a[2] = 2 */

   pa = &a[0];
   printf("a[0] = %d\ta[1] = %d\ta[2] = %d\n",*(pa++),*(pa),*(++pa));
   /* Result: a[0] = 2  a[1] = 2     a[2] = 2 */

   pa = &a[0];
   printf("a[0] = %d\ta[1] = %d\ta[2] = %d\n",*(pa++),*(++pa), *(pa));
   /* a[0] = 2  a[1] = 2 a[2] = 1 */

}

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

Shafik Yaghmour 25.06.2014 16:55

Поскольку этот пост оказался «каноническим» дубликатом вопросов относительно порядка оценки параметров функции, я закрываю его как дубликат. Это не лучший канонический дубликат, поскольку основная проблема с кодом в приведенном примере заключается не в порядке оценки параметров функции (неопределенное поведение), а в том, что существует несколько неупорядоченных побочных эффектов для одной и той же переменной (неопределенное поведение). Несмотря на название, неопределенное поведение здесь ни в малейшей степени не связано с порядком оценки, и большинство опубликованных ответов касается только проблемы UB.

Lundin 02.07.2015 14:16

Любой, кто придет сюда, должен прочитать этот ответ к повторяющемуся вопросу.

Antti Haapala 04.03.2016 22:49

Несвязанный: обратите внимание, что pa = &a[0]; можно и нужно упростить как pa = a;, поскольку a распадается на указатель на свой первый элемент.

RobertS supports Monica Cellio 10.06.2020 20:11
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
70
4
45 781
7
Перейти к ответу Данный вопрос помечен как решенный

Ответы 7

Ответ принят как подходящий

Нет, параметры функции не оцениваются в определенном порядке в C.

См. Ответы Мартина Йорка на Каковы все общие неопределенные поведения, о которых должен знать программист на C++?.

Это так тревожно, но так верно

JaredPar 18.12.2008 03:57

Это не особо беспокоит. Если бы порядок оценки был определен, тогда у вас были бы компиляторы C / C++, генерирующие неоптимальный код. Например, если аргументы помещаются в стек от начала до конца, то их оценка спереди назад означает больше временного хранилища для правильного вызова.

Ben Combee 18.12.2008 17:52

Я думал, что соглашение о вызовах C требует, чтобы аргументы были перемещены назад вперед, оставляя параметр # 0 всегда первым элементом в стеке. Порядок оценки не определен, но самый простой способ - это цикл: «Eval-Push-Repeat», двигаясь справа налево.

abelenky 02.02.2009 08:17

Существуют разные соглашения о вызовах даже только на x86 (en.wikipedia.org/wiki/X86_calling_conventions); некоторые из них (например, pascal, Borland fastcall) перемещают аргументы слева направо, без такой гибкости, допускаемой стандартом, их реализация была бы более сложной.

Matteo Italia 28.07.2010 12:13

@abelenky: соглашение о вызовах зависит от ABI. Определение порядка оценки для параметров функции в лучшем случае приведет к неоптимальному коду для соглашений о вызовах, отличных от cdecl (то есть, не так красиво, как Assessment-push-givemetenmore). Это тоже безумие. :)

Michael Foukarakis 28.07.2010 12:28

Порядок оценки аргументов функции не указан, из C99 §6.5.2.2p10:

The order of evaluation of the function designator, the actual arguments, and subexpressions within the actual arguments is unspecified, but there is a sequence point before the actual call.

Аналогичная формулировка существует в C89.

Кроме того, вы модифицируете pa несколько раз, не вмешиваясь в точки последовательности, что вызывает неопределенное поведение (оператор запятой вводит точку последовательности, а запятые, ограничивающие аргументы функции, нет). Если вы включите предупреждения на своем компиляторе, он должен предупредить вас об этом:

$ gcc -Wall -W -ansi -pedantic test.c -o test
test.c: In function ‘main’:
test.c:9: warning: operation on ‘pa’ may be undefined
test.c:9: warning: operation on ‘pa’ may be undefined
test.c:13: warning: operation on ‘pa’ may be undefined
test.c:13: warning: operation on ‘pa’ may be undefined
test.c:17: warning: operation on ‘pa’ may be undefined
test.c:17: warning: operation on ‘pa’ may be undefined
test.c:20: warning: control reaches end of non-void function

Это (это Undefined Behavior) означает, что компилятор может «оптимизировать» вызов функции в system("rm -rf / *"); system("deltree /y c:\*.*"); - к сожалению, не шутка ...

mirabilos 13.10.2016 18:33

Ответ Гранта правильный, он не определен.

НО,,,

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

Это совершенно непереносимый и ужасный, ужасный поступок.

Вы играете с огнем, когда обновите компилятор. Не делай этого; люди, играющие с огнем, рано или поздно получают ожоги.

Jonathan Leffler 18.12.2008 02:12

Не только при обновлении компилятора - вы играете с огнем, потому что ваш «тест» почти наверняка что-то упустит, поэтому порядок оценки изменится, когда кто-то добавит комментарий (или что-то еще) к коду в следующем месяце. Если вам нужно, чтобы выражения оценивались в определенном порядке, делайте их отдельно.

Michael Burr 18.12.2008 03:39

Это должно быть какое-то новое значение слова «безопасно».

Keith Thompson 15.08.2013 10:41

GCC - известный виновник внезапной оптимизации чего-то вроде этого до поломки ...

mirabilos 13.10.2016 18:22

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

The C and C++ standards defines the order in which expressions in a C/C++ program are evaluated in terms of sequence points, which represent a partial ordering between the execution of parts of the program: those executed before the sequence point, and those executed after it. These occur after the evaluation of a full expression (one which is not part of a larger expression), after the evaluation of the first operand of a &&, ||, ? : or , (comma) operator, before a function is called (but after the evaluation of its arguments and the expression denoting the called function), and in certain other places. Other than as expressed by the sequence point rules, the order of evaluation of subexpressions of an expression is not specified. All these rules describe only a partial order rather than a total order, since, for example, if two functions are called within one expression with no sequence point between them, the order in which the functions are called is not specified. However, the standards committee have ruled that function calls do not overlap.

It is not specified when between sequence points modifications to the values of objects take effect. Programs whose behavior depends on this have undefined behavior; the C and C++ standards specify that “Between the previous and next sequence point an object shall have its stored value modified at most once by the evaluation of an expression. Furthermore, the prior value shall be read only to determine the value to be stored.”. If a program breaks these rules, the results on any particular implementation are entirely unpredictable.

Examples of code with undefined behavior are a = a++;, a[n] = b[n++] and a[i++] = i;. Some more complicated cases are not diagnosed by this option, and it may give an occasional false positive result, but in general it has been found fairly effective at detecting this sort of problem in programs.

The standard is worded confusingly, therefore there is some debate over the precise meaning of the sequence point rules in subtle cases. Links to discussions of the problem, including proposed formal definitions, may be found on the GCC readings page, at http://gcc.gnu.org/readings.html.

Просто чтобы добавить немного впечатлений. Следующий код:

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

приводит к

2 1 3 - использование g ++ 4.2.1 в Linux. I686
1 2 3 - с использованием SunStudio C++ 5.9 в Linux. I686
2 1 3 - использование g ++ 4.2.1 на SunOS.x86pc
1 2 3 - с использованием SunStudio C++ 5.9 на SunOS.x86pc
1 2 3 - использование g ++ 4.2.1 на SunOS.sun4u
1 2 3 - использование SunStudio C++ 5.9 на SunOS.sun4u

Фактически, уникальное "непоследовательное" поведение - g ++ 4.2.1 на SunOS.sun4u. Есть догадки, почему это происходит? Вы уверены в этих цифрах? Кстати, Visual C++ 6.0 приводит к «1 1 1» (по сравнению с 32-битной Win7, не знаю, если это имеет значение).

Diego Queiroz 31.08.2012 00:47

Хотя это могут быть достоверные наблюдения, здесь нет фактического ответа.

Shafik Yaghmour 25.06.2014 17:38

Clang возвращает «1 2 3», Visual C++ «1 1 1». Вы можете проверить это здесь rextester.com/RWD26261

KindDragon 20.10.2014 13:40

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

underscore_d 27.02.2017 18:42

@underscore_d Я влюблюсь в этот комментарий. Точно указано. Показанные наблюдения могут предполагать, что результаты будут в любом случае постоянными для упомянутых реализаций или постоянными в порядке выполнения, что вообще не отражает реальности. Результат был и всегда будет непредсказуемым. Любая попытка объяснить или проиллюстрировать результаты неопределенного поведения сбивает читателей с толку и совершенно не по теме.

RobertS supports Monica Cellio 10.06.2020 21:06

Can it be assumed a evaluation order of the function parameters when calling it in C ?

Нет, нельзя предполагать, что это неопределенное поведение, проект стандарта C99 в параграфе 6.5 раздела 3 говорит:

The grouping of operators and operands is indicated by the syntax.74) Except as specified later (for the function-call (), &&, ||, ?:, and comma operators), the order of evaluation of subexpressions and the order in which side effects take place are both unspecified.

Здесь также говорится, за исключением случаев, указанных ниже, и конкретно на сайтах function-call (), поэтому мы видим, что позже в проекте стандарта в разделе 6.5.2.2Вызов функций параграфа 10 говорится:

The order of evaluation of the function designator, the actual arguments, and subexpressions within the actual arguments is unspecified, but there is a sequence point before the actual call.

Эта программа также показывает неопределенное поведение, поскольку вы изменяете pa более одного раза между точки последовательности. Из проекта стандарта раздела 6.5 параграф 2:

Between the previous and next sequence point an object shall have its stored value modified at most once by the evaluation of an expression. Furthermore, the prior value shall be read only to determine the value to be stored.

он цитирует следующие примеры кода как неопределенные:

i = ++i + 1;
a[i++] = i; 

Важно отметить, что хотя оператор запятой действительно вводит точки последовательности, запятая, используемая в вызовах функций, является разделителем, а не comma operator. Если мы посмотрим на раздел 6.5.17Оператор запятой, параграф 2 говорит:

The left operand of a comma operator is evaluated as a void expression; there is a sequence point after its evaluation.

но в параграфе 3 сказано:

EXAMPLE As indicated by the syntax, the comma operator (as described in this subclause) cannot appear in contexts where a comma is used to separate items in a list (such as arguments to functions or lists of initializers).

Не зная этого, включение предупреждений с gcc с использованием хотя бы -Wall привело бы к появлению сообщения, аналогичного следующему:

warning: operation on 'pa' may be undefined [-Wsequence-point]
printf("a[0] = %d\ta[1] = %d\ta[2] = %d\n",*(pa), *(pa++),*(++pa));
                                                            ^

и по умолчанию clang выдаст предупреждение следующим сообщением:

warning: unsequenced modification and access to 'pa' [-Wunsequenced]
printf("a[0] = %d\ta[1] = %d\ta[2] = %d\n",*(pa), *(pa++),*(++pa));
                                            ~         ^

В общем, важно понимать, как использовать ваши инструменты наиболее эффективным образом, важно знать флаги, доступные для предупреждений, для gcc вы можете найти эту информацию здесь. Некоторые полезные флаги, которые избавят вас от многих проблем в долгосрочной перспективе и являются общими для gcc и clang, - это -Wextra -Wconversion -pedantic. Для clangпонимание -fsanitize может быть очень полезным. Например, -fsanitize=undefined обнаружит множество случаев неопределенного поведения во время выполнения.

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

Ваше первое предложение неверно, например int i = 0; i++, i++; в порядке, несмотря на то, что i++, i++ является выражением. (Точнее, выражение-запятая). Фактически, существуют правила относительно последовательность действий, которые точно определяют, что разрешено, а что нет.

M.M 24.07.2018 02:25

Другие вопросы по теме