Что такое двойная оценка и почему ее следует избегать?

Я читал это в С++, используя макросы вроде

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

может привести к "двойной оценке". Может ли кто-нибудь дать мне пример того, когда происходит двойная оценка и почему это плохо?

PS: Удивительно, но я не мог найти подробного объяснения, когда он искал для него, за исключением примера в Clojure (чего я не могу понять).

Ответ 1

Представьте, что вы написали это:

#define Max(a,b) (a < b ? b : a)

int x(){ turnLeft();   return 0; }
int y(){ turnRight();  return 1; }

затем называет его следующим образом:

auto var = Max(x(), y());

Знаете ли вы, что turnRight() будет выполняться дважды? Этот макрос Max будет расширяться до:

auto var = (x() < y() ? y() : x());

После оценки условия x() < y() программа затем берет требуемую ветвь между y() : x(): в нашем случае true, которая вызывает y() во второй раз. Смотрите Live On Coliru.

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


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

PS: С++ имеет std::max функцию шаблона.

Ответ 2

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

max(++i, 4);

вернет 6, если i = 4 перед вызовом. Поскольку это не ожидаемое поведение, вы должны предпочесть встроенные функции для замены таких макросов, как max.

Ответ 3

Рассмотрим следующее выражение:

 x = max(Foo(), Bar());

Где Foo и Bar выглядят следующим образом:

int Foo()
{
    // do some complicated code that takes a long time
    return result;
}

int Bar()
{
   global_var++;
   return global_var;
}

Затем в исходном выражении max расширяется как:

 Foo() > Bar() ? Foo() : Bar();

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

Ответ 4

Язык макросов в C и С++ обрабатывается выделенным парсером на этапе предварительной обработки; токены переводятся и вывод затем подается во входной поток самого анализатора. #define и #include токены не распознаются самими синтаксическими анализаторами C или С++.

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

#define MAX(A, B) (A > B ? A : B)

int i = 1, j = 2;
MAX(i, j);

что видит синтаксический анализатор С++,

(i > j ? i : j);

Однако, если мы используем макрос с чем-то более сложным, происходит такое же расширение:

MAX(i++, ++j);

расширяется до

(i++ > ++j ? i++ : ++j);

Если мы передаем что-то, что вызывает вызов функции:

MAX(f(), g());

это будет расширяться до

(f() > g() ? f() : g());

Если компилятор/оптимизатор может продемонстрировать, что f() не имеет побочных эффектов, тогда он будет рассматривать это как

auto fret = f();
auto gret = g();
(fret > gret) ? fret : gret;

Если он не может, тогда ему придется дважды вызывать функции f() и g(), например:

#include <iostream>

int f() { std::cout << "f()\n"; return 1; }
int g() { std::cout << "g()\n"; return 2; }

#define MAX(A, B) (A > B ? A : B)

int main() {
    MAX(f(), g());
}

Live demo: http://ideone.com/3JBAmF

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