О присваивании (1)
Предположим, у нас следующий код:
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(), таким образом, была вычислена один раз.
Вывод таков, что даже если выражение записывается кратко, оно занимает несколько (порой много) машинных команд. Может, кому-то это очевидно, но не мешает лишний раз проговорить.
Нашли ошибку? Выделите мышкой и нажмите Ctrl/⌘+Enter