Преимущество использования составного присваивания

Каково реальное преимущество использования составного присваивания в C/C++ (или может быть применимо и ко многим другим языкам программирования)?

#include <stdio.h>

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

    return 0;
};

Я просмотрел несколько ссылок, таких как сайт Microsoft. , SO post1, SO Post2 . Но преимущество говорит, что exp1 оценивается только один раз в случае составного оператора. Как exp1 действительно оценивается дважды в первом случае? Я понимаю, что сначала считывается текущее значение exp1, а затем добавляется новое значение. Обновленное значение записывается обратно в то же место. Как это действительно происходит на более низком уровне в случае составного оператора? Я попытался сравнить ассемблерный код двух корпусов, но не увидел между ними никакой разницы.


person Rajesh    schedule 19.04.2018    source источник
comment
Есть большая вероятность, что ваш компилятор оптимизировал его. В любом случае, язык ассемблера обычно имеет инкрементные коды операций. На самом деле ничего другого у них нет. Если вы сделаете что-то вроде 1 + 2 в C, это будет скомпилировано во что-то вроде move 1,a и add 2,a.   -  person SeverityOne    schedule 19.04.2018
comment
Ответ @SeverityOne должен быть ответом...   -  person Frederick    schedule 19.04.2018


Ответы (6)


Для простых выражений, включающих обычные переменные, разница между

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 */

Наконец (спасибо Йенсу Густедту за напоминание об этом), мы должны вернуться назад и подумать немного более тщательно о том, что мы имели в виду, когда сказали: «Что становится интересным, так это когда левая часть задания представляет собой выражение, включающее побочные эффекты." Обычно мы думаем о модификациях как о побочных эффектах, а доступы — как о «бесплатных». Но для переменных, определяемых как volatile (или, в C11, как _Atomic), доступ также считается интересным побочным эффектом. Таким образом, если переменная a имеет один из этих квалификаторов, a = a + b не является «простым выражением, включающим обычные переменные», и, в конце концов, оно может быть не таким уж идентичным a += b.

person Steve Summit    schedule 19.04.2018
comment
Получил реальную проблему сейчас. Я думаю, что большинство книг не освещают эту концепцию двухвременной оценки, приводя хороший пример. На самом деле это самое главное отличие, чем читабельность. @dbush также дал еще один хороший пример, где мы можем легко увидеть результат двух временных оценок. Недостаток, обозначенный chux, также дает нам лучшую ясность. - person Rajesh; 19.04.2018
comment
Нет, они не всегда одинаковы. Это зависит от того, что такое a. Если это volatile или _Atomic, результат принципиально другой. - person Jens Gustedt; 19.04.2018
comment
Как бы tmp = *XYZ; интерпретировать и почему? мой компилятор интерпретирует это как: Скопировать *XYZ в temp, а почему бы и нет tmp = tmp * XYZ ? Примечание: XYZ — это указатель. - person M Sharath Hegde; 30.08.2018
comment
@MSharathHegde Если бы составной оператор присваивания был записан как =*, здесь может быть некоторая двусмысленность, но, поскольку на самом деле он пишется как *=, совершенно ясно, что tmp=*XYZ должен включать доступ к указателю, а не умножение. (Теперь, в первые дни C, составной оператор присваивания был записан как =*, и была двусмысленность, поэтому составные операторы присваивания были преобразованы в их современную форму. .) - person Steve Summit; 30.08.2018

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

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;

Который вызывает функцию дважды.

person dbush    schedule 19.04.2018

Вот что говорит стандарт 6.5.16.2:

Составное присваивание формы E1 op= E2 эквивалентно простому выражению присваивания E1 = E1 op (E2), за исключением того, что lvalue 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
}

Версия с простым присваиванием не только давала другой результат, но и вводила в код неопределенное поведение, так что в зависимости от компилятора возможны два разных результата.

person Lundin    schedule 19.04.2018
comment
правильный. в дополнение к volatile это также может иметь значение для атомарных квалифицированных типов. - person Jens Gustedt; 19.04.2018

Не уверен, что вы после. Составное присваивание короче и, следовательно, проще (менее сложно), чем использование обычных операций.

Учти это:

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

против:

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

Какой из них легче прочитать, понять и проверить?

Для меня это очень-очень реальное преимущество, и оно так же верно, независимо от семантических деталей, например, сколько раз что-то оценивается.

person unwind    schedule 19.04.2018
comment
Возможно, главная проблема с вашим примером заключается в том, что вы не следуете закону Деметры. Разница между x += dt * speed и x =x + dt * speed меньше, но все равно стоит иметь. - person Pete Kirkham; 19.04.2018
comment
Я согласен с этим ответом. Современные компиляторы обычно не нуждаются в помощи программистов, использующих определенные языковые конструкции. Ведь сегодня лучшая читабельность фрагмента кода обычно означает и лучшую оптимизацию компилятором. - person Blue; 19.04.2018
comment
@unwind Я в основном говорю о проблемах, связанных с производительностью, а не о читабельности. Согласно Microsoft, однако, составное выражение присваивания не эквивалентно расширенной версии, поскольку составное выражение присваивания оценивает выражение1 только один раз, тогда как расширенная версия оценивает выражение1 дважды: в операции сложения и в операции присваивания. Вот я и жду каких-то объяснений. - person Rajesh; 19.04.2018

Преимущество использования составного присваивания

Есть и недостаток.
Учитывайте влияние типов.

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;
person chux - Reinstate Monica    schedule 19.04.2018
comment
даже clang -Weverything не выдает предупреждения... так и должно быть! - person Stargateur; 19.04.2018

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

move 1,a
add 2,a

Это то, во что 1+2 будет компилироваться на ассемблере. Очевидно, это, возможно, чрезмерно упрощено, но вы поняли идею.

Кроме того, компилятор имеет тенденцию оптимизировать ваш код, поэтому exp1=exp1+b, скорее всего, будет компилироваться с теми же кодами операций, что и exp1+=b.

И, как заметил @unwind, составной оператор намного читабельнее.

person SeverityOne    schedule 19.04.2018
comment
Я использую компилятор TDM-GCC-64. Оптимизация отключена. Вот журнал 'gcc.exe -Wall -s -pedantic -Wextra -Wall -g -c C:\sample_Project_Only_Main\main.c -o Debug\main.o g++.exe -o Debug\sample_Project_Only_Main.exe Debug\main .о - person Rajesh; 19.04.2018
comment
Справедливо. Тем не менее, ассемблер всегда работает по принципу добавления чего-либо в регистр или ячейку памяти, что и делает оператор +=. - person SeverityOne; 19.04.2018