什么是双重求值,为什么应该避免使用它?

56

我看到有人在使用C++的宏

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

“double evaluation”可能导致问题。有人能举个例子说明何时会出现双重评估,以及为什么它是不好的吗?

P.S.:令人惊讶的是,在谷歌搜索中我找不到任何详细的解释,除了在Clojure中的一个例子(我无法理解)。


23
我会将这句话翻译为:“我对此的理解与你不同:如果a和b是复杂的或者是函数调用,它们将被评估多次。这对性能来说不好,并且甚至可能导致错误。” - Jean-François Fabre
5
如果ab是带有副作用的函数调用会怎样?如果abx++++x会怎样? - James M
4
请考虑以下代码:pips = max(rand(), 5) + 1; // 投掷骰子。这能保证返回一个在[1,6]之间的整数吗?如果max()是涉及双重求值的宏定义,则不能保证。 - njuffa
1
@JohannesSchaub-litb,这个操作符简单地说“可能导致双重评估”,你显然足够聪明,可以推断出条件语句取决于使用情况,例如max(i, a.pop_and_ret())max(x++, y++),在预处理期间会扩展为(x++ > y++ ? x++ : y++),因此优化器不知道有宏扩展。 - kfsone
1
除了双重评估问题之外,您的代码还存在运算符优先级问题。您应该在参数周围加上括号以防止出现这种情况。例如:#define max(a,b) ((a) > (b) ? (a) : (b)) - CodesInChaos
显示剩余16条评论
4个回答

77

想象一下你写了这个:

#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
简单来说,将 表达式 作为参数传递给您的 函数宏 Max 可能会导致该表达式被评估两次,因为该表达式将在宏参数所采用的所有位置上重复使用,在宏定义中使用。记住,宏由预处理器处理。

所以,底线是,不要使用宏定义函数(实际上在这种情况下是表达式),仅仅因为你想让它变得通用,而可以通过使用函数模板有效地实现。

附注:C++有一个std::max模板函数。


@PeterMortensen 在C++中一直存在着std::max模板函数,尽管它已经根据最近的C++版本进行了修改。 - WhiZTiM
2
@PatrickM'Bongo:这两个观点似乎都没有什么意义。std::max<double>(12.f,13.)是消除歧义的正常方式。而且没有任何函数可以延长其参数的生命周期,这不是 C++ 的工作方式。虽然这并不重要:引用直到完整表达式的末尾才会悬空。只有在使用它初始化另一个引用时才会产生影响,那时你显然要对你的引用负责。 - MSalters
@MSalters,您应该意识到,只需将“max”作为可变参数模板采用转发引用而不是当前的无意义方法,这个问题就可以轻松解决,对吧? - Griwes
2
在正常情况下,您应该返回common_type_t<Ts...>,因为这可以使用?:免费实现。 - Griwes
1
注意,在C语言中,gcc和clang支持语句表达式的扩展,可以用于避免双重求值问题。在C++中,我们有其他可用的技术。 - Shafik Yaghmour
显示剩余3条评论

24

ab在宏定义中出现了两次。因此,如果您将其与具有副作用的参数一起使用,则副作用会执行两次。

max(++i, 4);

如果在调用之前i = 4,将会返回6。因为这不是期望的行为,所以你应该优先使用内联函数来替换这样的宏,比如max


4
同时,这并不是未定义行为 ((++i) < (4) ? (++i) (4)),因为在评估第一个 ++i 后存在 序列点。然而,在同一行上对同一变量进行多次增量操作可能通常不是一个好主意。 - Daerdemandt

21
考虑以下表达式:
 x = max(Foo(), Bar());

其中FooBar是这样的:

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示例中,它不能始终返回相同的值。


8
在C和C++中,宏语言在“预处理”阶段由专用解析器处理。这些标记被翻译,然后输出被馈送到解析器的输入流中。 C或C++解析器本身不识别#define和#include标记。 这一点很重要,因为它意味着当说一个宏被“展开”时,它的意思是字面上的。给定...
#define MAX(A, B) (A > B ? A : B)

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

C++解析器看到的是

(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());
}

实时演示: http://ideone.com/3JBAmF

同样地,如果我们调用一个extern函数,优化器可能无法避免调用函数两次


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接