使用复合赋值的优势

Advantage of using compound assignment

在 C/C++ 中使用复合赋值的真正优势是什么(或者也可能适用于许多其他编程语言)?

#include <stdio.h>

int main()
{
    int exp1=20;
    int b=10;
   // exp1=exp1+b;
    exp1+=b;

    return 0;
};

我看了几个像 microsoft site, SO post1, SO Post2 这样的链接。 但优点是 exp1 在复合语句的情况下只被评估一次。在第一种情况下,exp1 是如何真正被评估两次的?我知道先读取 exp1 的当前值,然后再添加新值。更新后的值被写回到相同的位置。在复合语句的情况下,这实际上是如何在较低级别发生的?我试着比较了两种情况的汇编代码,但我没有看到它们之间有什么区别。

不确定您在寻找什么。复合赋值比使用常规操作更短,因此更简单(更简单)。

考虑一下:

player->geometry.origin.position.x += dt * player->speed;

对比:

player->geometry.origin.position.x = player->geometry.origin.position.x + dt * player->speed;

哪一个更容易阅读理解和验证?

对我来说,这是一个非常非常真实的优势,并且无论语义细节如何,例如某物被评估了多少次,都是如此。

像 C 这样的语言总是会成为底层机器操作码的抽象。在加法的情况下,编译器首先将左操作数移入累加器,然后将右操作数添加到累加器中。像这样(伪汇编代码):

move 1,a
add 2,a

这就是 1+2 在汇编程序中编译的结果。显然,这可能过于简单化了,但你明白了。

此外,编译器倾向于优化您的代码,因此 exp1=exp1+b 很可能会编译为与 exp1+=b 相同的操作码。

而且,正如@unwind 所说,复合语句的可读性更高。

对于涉及普通变量的简单表达式,两者的区别

a = a + b;

a += b;

只是句法。这两个表达式的行为完全相同,并且很可能生成相同的汇编代码。 (你是对的;在这种情况下,询问 a 是否被评估一次或两次甚至没有多大意义。)

有趣的是赋值的左侧是一个涉及副作用的表达式。所以如果你有类似

*p++ = *p++ + 1;

对比

*p++ += 1;

它带来了更多不同!前者尝试将 p 递增两次(因此未定义)。但后者恰好计算了一次 p++,并且定义明确。

正如其他人所提到的,还有符号方便和可读性的优点。如果你有

variable1->field2[variable1->field3] = variable1->field2[variable2->field3] + 2;

很难发现错误。但是如果你使用

variable1->field2[variable1->field3] += 2;

不可能连那个bug都没有,后来的reader也不用仔细检查条款就排除了这种可能性。

一个小优点是它可以为您节省一对括号(或者如果您将这些括号排除在外,则不会出现错误)。考虑:

x *= i + 1;         /* straightforward */
x = x * (i + 1);    /* longwinded */
x = x * i + 1;      /* buggy */

最后(感谢 Jens Gustedt 提醒我这一点),我们必须回过头来更仔细地思考一下我们说 "Where it gets interesting is when the left-hand side of the assignment is an expression involving side effects." 时的意思效果和访问 "free"。但是对于限定为 volatile(或者在 C11 中,限定为 _Atomic)的变量,访问也算作一个有趣的副作用。因此,如果变量 a 具有这些限定符之一,则 a = a + b 不是 "simple expression involving ordinary variables",毕竟它可能与 a += b 不太相同。

如果左侧不仅仅是一个简单的变量名,则对左侧求值一次可以节省很多。例如:

int x[5] = { 1, 2, 3, 4, 5 };
x[some_long_running_function()] += 5;

在这种情况下 some_long_running_function() 只被调用一次。这不同于:

x[some_long_running_function()] = x[some_long_running_function()] + 5;

调用函数两次。

标准6.5.16.2是这么说的:

A compound assignment of the form E1 op= E2 is equivalent to the simple assignment expression E1 = E1 op (E2), except that the lvalue E1 is evaluated only once

所以 "evaluated once" 就是区别。这在您具有 volatile 限定符并且不想多次读取硬件寄存器的嵌入式系统中很重要,因为这可能会导致不必要的副作用。

这真的不可能在 SO 上重现,所以这里有一个人工示例来说明为什么多次评估会导致不同的程序行为:

#include <string.h>
#include <stdio.h>

typedef enum { SIMPLE, COMPOUND } assignment_t;

int index;

int get_index (void)
{
  return index++;
}

void assignment (int arr[3], assignment_t type)
{
  if(type == COMPOUND)
  {
    arr[get_index()] += 1;
  }
  else
  {
    arr[get_index()] = arr[get_index()] + 1;
  }
}

int main (void)
{
  int arr[3];

  for(int i=0; i<3; i++) // init to 0 1 2
  {
    arr[i] = i;
  }
  index = 0;
  assignment(arr, COMPOUND);
  printf("%d %d %d\n", arr[0], arr[1], arr[2]);   // 1 1 2

  for(int i=0; i<3; i++) // init to 0 1 2
  {
    arr[i] = i;
  }
  index = 0;
  assignment(arr, SIMPLE);
  printf("%d %d %d\n", arr[0], arr[1], arr[2]);   // 2 1 2 or 0 1 2
}

简单赋值版本不仅给出了不同的结果,还在代码中引入了未指定的行为,因此可能会出现两种不同的结果,具体取决于编译器。

Advantage of using compound assignment

也有一个缺点
考虑类型的影响。

long long exp1 = 20;
int b=INT_MAX;

// All additions use `long long` math
exp1 = exp1 + 10 + b;

10 + b 下面的加法将使用 int 数学和溢出(未定义的行为

exp1 += 10 + b;  // UB 
// That is like the below,
exp1 = (10 + b) + exp1;