Предположим, у нас следующий код:

x += 5

Если спросить программиста, как он устроен, то скорее всего, ответ будет таким: это то же самое, что

x = x + 5

Во-первых, это не верно, а во-вторых, такой ответ все равно ничего не объясняет.

Ошибка кроется в следующем. Если предположить, что x += 5 означает x = x + 5, то из этого можно сделать вывод: любое выражение <whatever> += 5 под капотом становится

<whatever> = <whatever> + 5

Представим теперь, что есть массив m, и нужно увеличить его элемент на пять. Индекс этого элемента дает функция f без параметров. Таким образом, я запишу:

m[f()] += 5

Если следовать ошибочному предположению выше, это выражение становится таким:

m[f()] = m[f()] + 5

Видим, что функция f() вызывается дважды: сначала справа, чтобы посчитать значение, затем слева, чтобы присвоить его. Предположим теперь, функция f() возвращает индекс случайно: в первый раз 2, а во второй — 7. Итоговое выражение станет таким:

m[7] = m[2] + 5

что совершенно неправильно. Так что тезис о том, что x += 5 равносильно x = x + 5 — ошибочный.

Если рассмотреть выражение x = x + 5 внимательней, станет ясно: у символа x в зависимости от положения слева или справа равенства разная семантика. Справа он используется для того, чтобы получить значение, а слева — адрес, по которому нужно его записать.

В стандартах языков Си и Си++ для этого служат термины lvalue и rvalue, что переводится как “лево-“ и “праводопустимые значения”. Так, на месте lvalue (слева от равенства) может быть любое выражение, от которого можно взять адрес. У правого выражения требования другие, и взятие адреса не требуется. Именно поэтому запись x = 5 верная: от x можно взять адрес. По той же причине 5 = x не пройдет компиляцию: от литерала 5 взять адрес нельзя.

Даже если упростить выражение до предела и оставить x = x, у обоих иксов будет разная семантика. Тот, что слева означает адрес, а справа — значение.

Имея все это в виду, посмотрим, что происходит с выражением m[f()] += 5. Его левая часть вычисляется, чтобы получить адрес, куда позже поместится результат. Если бы мы написали просто x += 5, то адрес x вычислять не надо — компилятор уже его знает. Однако для m[f()] понадобится вычислить f() и за счет адресной арифметики получить конечный адрес.

Далее: значение из этого адреса помещается в отдельный регистр для вычислений, например EAX. К этому регистру добавляется пятерка командой ADD EAX 5 (добавить что-то к ячейке памяти нельзя, только к регистру). После этого значение из EAX перемещается в тот адрес, что был вычислен в левом выражении. Функция f(), таким образом, была вычислена один раз.

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