Макросы и постинкремент

Вот еще немного странного поведения макросов, на которое я надеялся, кто-нибудь сможет пролить свет:

#define MAX(a,b) (a>b?a:b)

void main(void)
{
  int a = 3, b=4;

  printf("%d %d %d\n",a,b,MAX(a++,b++));
}

Результатом является 4 6 5. Значение b увеличивается дважды, но не раньше, чем MAX отобразит его значение. Может ли кто-нибудь сказать мне, почему это происходит и как можно предсказать такое поведение? (Еще один пример того, почему макросов следует избегать!)


person Appster    schedule 15.09.2011    source источник
comment
интересно, что произойдет, если вы сделаете MAX(++a, ++b)? я нашел эту информацию на форуме: никогда не используйте предварительное или последующее увеличение в качестве параметров макросов, так как макрос может использовать параметр более одного раза. Как следствие, пункт 2 никогда не должен использовать увеличение или уменьшение параметров функции, так как вы можете не знать, является ли это реальной функцией или она была реализована как макрос. источник: bytes.com/topic/c/answers/   -  person peko    schedule 15.09.2011
comment
Это можно считать аргументом против использования пре- или постинкремента.   -  person phkahler    schedule 15.09.2011
comment
@peko: при этом, когда стандартные функции реализованы в виде макросов, они должны оценивать каждый аргумент ровно один раз (7.1.4/1). Кроме того, стандартные библиотеки всегда предоставляют функцию а также макрос. Таким образом, strlen(++s) увеличивает s ровно один раз, но если вы все равно хотите избежать макроса, вы можете вместо этого написать (strlen)(++s), что не является информацией о макросе. Авторы сторонних библиотек могут предоставлять или не предоставлять эти полезные гарантии, если они предоставляют вещи, о которых они не сообщают вам, являются ли они функциями или макросами.   -  person Steve Jessop    schedule 15.09.2011


Ответы (8)


Макросы делают замену текста. Ваш код эквивалентен:

printf("%d %d %d\n",a,b, a++ > b++ ? a++ : b++);

Это имеет неопределенное поведение, поскольку b потенциально увеличивается (в конце третьего аргумента), а затем используется (во втором аргументе) без промежуточной точки последовательности.

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

Такое поведение ненадежно — другая реализация или та же реализация в другой день может дать другие результаты из-за оценки аргументов в другом порядке, или теоретически может даже произойти сбой из-за проблемы с точкой следования.

person Steve Jessop    schedule 15.09.2011
comment
Я как бы понимаю, что происходит, когда у меня есть вывод, но как мне предсказать такое поведение? - person Appster; 15.09.2011
comment
@Appster: вы не можете (без использования множества недокументированных деталей вашего конкретного компилятора), это неопределенное поведение. Никогда не пишите этот код. Если вы имеете в виду, как я могу предсказать, что он не определен, то упрощенная версия заключается в том, что вы никогда не должны использовать переменную, а также увеличивать ее в том же выражении. Если на собеседовании задают вопрос, что выводит эта программа?, то правильный ответ - мне все равно, а если вам все равно, то вы плохой программист на C. Возможно, это не тот ответ, который, скорее всего, даст вам работу, но он правильный :-) - person Steve Jessop; 15.09.2011
comment
+1 . Получается, что a++ › b++ сравнивает 3 › 4? что это не так, поэтому компилятор смещается вправо от : после увеличения a и b до 4 и 5, соответственно. Таким образом, результат MAX при отображении равен b=5, а затем b увеличивается во второй раз. Думаю, я понял! - person Appster; 15.09.2011
comment
@Appster: это правильно, и MAX(a++,b++) сам по себе определил (хотя и удивительно) поведение, поскольку условный оператор ?: имеет точку последовательности между условием и любым из двух других выражений, которые фактически оцениваются. Это комбинация MAX с дополнительным использованием a и b в том же выражении, которое не определено. - person Steve Jessop; 15.09.2011
comment
Когда вы работаете с C, вы замечаете много недокументированного поведения, но тщательное наблюдение (и множество тестовых образцов) всегда выявляет закономерность. Это то, на чем преуспевают интервьюеры, и, сказав интервьюеру, что мне все равно, меня точно вышвырнут :) - person Appster; 15.09.2011
comment
Нет ничего плохого в том, чтобы использовать переменную и увеличивать ее в одном и том же выражении — просто не делайте этого ДВАЖДЫ в одном и том же выражении. - person Dmitri; 15.09.2011
comment
@Appster: Хорошо, все равно, возможно, это немного сильно, но все ваши тщательные наблюдения говорят вам только о том, что, по-видимому, делает одна реализация. Используйте другой компилятор, или другие флаги оптимизации, или левую руку вместо правой, чтобы нажать Enter, это может сделать что-то другое. Вот почему, формально говоря, меня не волнует, что делает код, даже если на самом деле я достаточно любопытен, чтобы выработать объяснение. - person Steve Jessop; 15.09.2011
comment
@Dmitri: да, для краткости я сказал использовать, на самом деле это использование, кроме как в приращении. Так, например, a + (++a) имеет неопределенное поведение, потому что a используется не в приращении. b = ++a, конечно, не имеет неопределенного поведения, хотя увеличение a по своей сути использует a. ++a && a также имеет определенное поведение благодаря точке последовательности в операторе &&, но если вы хотите полагаться на это, вы должны изучить его должным образом, а не использовать мое упрощенное правило. - person Steve Jessop; 15.09.2011
comment
@Steve: думаю, это лучшее объяснение. Не используйте, кроме как в приращении. Следовать более простой версии было бы действительно неудобно. - person Dmitri; 15.09.2011
comment
@SteveJessop Я пытаюсь понять, почему printf(%d %d %d\n,a,b,MAX(a+1,b+1)); работает, похоже компилятор с этим справляется нормально. В результате получается a: 3, b: 4 и MAX 5. -- Я полагаю, это потому, что x+1 и y+1 не изменяют значения x и y соответственно? - person codingManiac; 25.06.2017

В макросе параметры просто заменяются аргументами; поэтому аргументы могут оцениваться несколько раз, если они присутствуют в макросе несколько раз.

Ваш пример:

MAX(a++,b++)

Расширяется до этого:

a++>b++?a++:b++

Я думаю, вам не нужно больше пояснений :)

Вы можете предотвратить это, назначив каждому параметру временную переменную:

#define MAX(a,b) ({   \
    typeof(a) _a = a; \
    typeof(b) _b = b; \
    a > b ? a : b;    \
})

(Однако этот использует несколько расширений GCC)

Или используйте встроенные функции:

int MAX(int a, int b) {
    return a > b ? a : b;
}

Это будет так же хорошо, как макрос во время выполнения.

Или не делайте приращения в аргументах макроса:

a++;
b++;
MAX(a, b)
person Arnaud Le Blanc    schedule 15.09.2011
comment
согласовано. часто гораздо безопаснее избегать макросов, как во встроенной функции в этом ответе, потому что они безопасны для типов. кроме того, поскольку они являются встроенными, они на самом деле не приводят к созданию и уничтожению кадра стека, как это сделала бы обычная функция. - person Max DeLiso; 15.09.2011
comment
Типобезопасны, оценивают свои аргументы только один раз, видны большему количеству отладчиков… Только учтите, что использование здесь функции вместо макроса не удаляет UB. - person Christopher Creutzig; 16.09.2011

Когда препроцессор читает строку, он заменяет MAX(a++,b++) в printf на (a++>b++?a++;b++)

Таким образом, ваша функция становится

    printf(a,b,(a++>b++?a++;b++));

Здесь порядок оценки "зависит от компилятора".

Чтобы понять, когда могут возникнуть эти условия, вам нужно понять точку последовательности.

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

    a[i] = i++;

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

--Точки последовательности, изложенные в Стандарте, следующие:

1) Точка вызова функции, после оценки ее аргументов.

2) Конец первого операнда оператора &&.

3)Конец первого операнда || оператор.

4) Конец первого операнда условного оператора ?:.

5) Конец каждого операнда оператора запятой.

6) Завершение оценки полного выражения. Они следующие:

Оценка инициализатора автоматического объекта.

Выражение в «обычном» операторе — выражение, за которым следует точка с запятой.

Управляющие выражения в операторах do, while, if, switch или for.

Два других выражения в операторе for.

Выражение в операторе возврата.

person Anurag    schedule 15.09.2011

Макросы оцениваются препроцессором, который тупо заменяет все в соответствии с определениями макросов. В вашем случае MAX(a++, b++) становится (a++>b++) ? a++ : b++.

person glglgl    schedule 15.09.2011

Если я прав, это происходит:

с заменой MAX на (a>b...) у вас есть printf("%d %d %d\n",a,b,(a++ > b++ ? a++ : b++ ) );

Сначала проверяется a++ > b++, а затем оба значения увеличиваются (a = 4, b = 5). Затем становится активным второй b++, но поскольку он является постинкрементным, он увеличивается после вывода второго значения b = 5.

Извините за мой плохой английский, но я надеюсь, вы его понимаете?! :D

Привет из Германии ;-)

Ральф

person Ralf Kuehn    schedule 15.09.2011

Итак, ваше расширение дает (с поправкой на ясность):

(a++ > b++) ? a++ : b++

... поэтому (a++ > b++) оценивается первым, давая по одному приращению и выбирая ветвь на основе еще не увеличенных значений a и b. Выбрано выражение «иначе», b++, которое выполняет второе приращение к b, которое уже было увеличено в тестовом выражении. Поскольку это постинкремент, значение b перед вторым инкрементом присваивается printf().

person Dmitri    schedule 15.09.2011


Есть две причины для результата, который вы получаете здесь:

  1. Макрос — это не что иное, как код, который расширяется и вставляется при компиляции. Итак, ваш макрос

    MAX(a,b) (a>b?a:b)
    

    становится этим

    a++>b++?a++:b++
    

    и ваши результирующие функции таковы:

    printf("%d %d %d\n",a,b, a++>b++?a++:b++);
    

    Теперь должно быть понятно, почему b увеличивается дважды: сначала при сравнении, а затем при возврате. Это не неопределенное поведение, оно хорошо определено, просто проанализируйте код, и вы увидите, чего именно ожидать. (это как-то предсказуемо)

  2. Вторая проблема заключается в том, что в C параметры передаются в стек от последнего к первому, поэтому все приращения будут выполняться до того, как вы напечатаете исходные значения a и b, даже если они указаны первыми. Попробуйте эту строку кода, и вы поймете, что я имею в виду:

    int main(void)
    {
        int a = 3, b=4;
        printf("%d %d %d\n",a,b, b++);
        return 0;
    }
    

Вывод будет 3 5 4. Я надеюсь, что это объясняет поведение и поможет вам предсказать результат.
НО последний пункт зависит от вашего компилятора

person unexplored    schedule 15.09.2011
comment
в C параметры передаются от последнего к первому в стек, возможно, в ABI, используемом вашей реализацией, но стандарт этого не гарантирует. Даже реализация не определяет, в каком порядке оцениваются аргументы, это не указано. - person Steve Jessop; 15.09.2011

Я думаю, что спрашивающий ожидал, что вывод начнется:

3 4 ...

вместо:

4 6 ...

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

Я думаю (и кто-то опубликует комментарий, если это неправильно), что это определено в стандарте C (и C++).

Обновить

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

person Skizz    schedule 15.09.2011
comment
6.5.2.2/10 из C99: Порядок оценки указателя функции, фактических аргументов и подвыражений в фактических аргументах не определен, но перед фактическим вызовом есть точка последовательности. - person Steve Jessop; 15.09.2011