4

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

#include <stdio.h>

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

    return 0;
};

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

4

6 回答 6

3

对于涉及普通变量的简单表达式,

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;

甚至不可能有那个错误,以后的读者不必仔细检查这些条款来排除这种可能性。

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

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

最后(感谢 Jens Gustedt 提醒我这一点),我们必须回过头来更仔细地思考一下我们所说的“有趣的地方是当作业的左侧是一个涉及副作用。” 通常,我们认为修改是副作用,而访问是“免费的”。但是对于限定为volatile(或在 C11 中为 as _Atomic)的变量,访问也算作一个有趣的副作用。因此,如果 variablea具有这些限定符之一,则不是“涉及普通变量的简单表达式”,毕竟a = a + b它可能与 不完全相同。a += b

于 2018-04-19T13:54:16.423 回答
2

如果它不仅仅是一个简单的变量名,那么对左侧进行一次评估可以为您节省很多。例如:

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;

它调用该函数两次。

于 2018-04-19T14:05:30.823 回答
2

这就是标准 6.5.16.2 所说的:

E1 op = E2形式的复合赋值等价于简单赋值表达式E1 = E1 op ( E2 ),除了左值E1只计算一次

所以“评估一次”是不同的。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
}

简单赋值版本不仅给出了不同的结果,它还在代码中引入了未指定的行为,因此根据编译器的不同可能产生两种不同的结果。

于 2018-04-19T14:22:57.287 回答
1

不知道你在追求什么。复合赋值比使用常规操作更短,因此更简单(不太复杂)。

考虑一下:

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

相对:

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

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

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

于 2018-04-19T13:31:59.550 回答
1

使用复合赋值的优点

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

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;
于 2018-04-19T14:34:16.633 回答
0

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

move 1,a
add 2,a

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

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

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

于 2018-04-19T13:32:52.243 回答