Pull to refresh

Comments 48

id 188888 — юбилейный пост.

А вообще, об этом уже писали на хабре. Насколько я помню, автор закрыл пост.
И время поста кстати тоже удачное.

Что интересно, постов с номерами 188887, 188886 и т.д. нет.
> Первое, что делает компилятор, когда видит код z += y это преобразует его в z = z + y
И это не совсем верно.

Правильный ответ 6. x += x будет выполнен. ++ пролетит мимо.
Не пролетит мимо, а выполнится сразу после присвоения.
Поэтому 7.
В C++ будет 7, а в C# — 6, так что вы наполовину правы
в C++ это неопределённое поведение, поскольку внутри одной точки следования дважды присваивается значение одной переменной.
В топике разговор про C#, так что ответ именно про него.
В Java аналогично, если мне не изменяет память.
Но для начала, НИКОГДА НЕ ПИШИТЕ ТАКОЙ КОД.

Где подписаться? В самом деле, если код вызывает затруднения при прочтении и понимании, и, более того, может иметь неоднозначную интерпретацию в разных ЯП — вот сразу нахрен такой код, а автора бить нещадно клавиатурой по руках.
На собеседованиях почему-то такое любят. Под соусом «у нас есть легаси-код, в нем нужно иногда разобраться и пофиксить баги».
Тогда только от вас зависит, будут ли идиотами все присутствующие на собеседовании или не все )
Иногда, пока не сходишь, не поймешь:)
из недавнего: printf("%d\n", (int*)3 + 2);
оно настолько UB что попытка узнать что оно выведет на экран покажет познания автора в устройстве процессора: ширина шины, устройство виртуальной памяти, гранулярность доступа (на mips), и тд
(int*)3 + 2 — это (int*)11 при 32х-разрядном int.

Если архитектура 32х-битная с плоской моделью памяти, то именно это 11 и будет выведено. Но если используется 64х-битная архитектура или полная модель памяти, то размер указателя будет больше размера int, и в итоге будут выведены крайние 4 байта (которые могут быть 11ю, старшим нулем или селектором регистра данных в зависимости от того, что окажется сверху стека).

Еще больше ответов на этот вопрос возникает, если вспомнить про старые компиляторы, где int может быть 16ти-битным.
Не понимаю, почему столько людей выбрало 7?
x += n всегда разворачивается в x = x+n, а не n+x.
К чему эти теоретические рассуждения? Поведение должно быть предсказуемым, и оно предсказуемо, исходя из определения операции +=.
Что, в каком-то ЯП x += n определено как x = n + x?
Шаг «разворачивания в x+n» не является для людей очевидным. Если бы его не было, то должно было бы получиться 7:
[x = 3] x += (x++)
[x = 4] x += 3
[x = 7]

Что, в каком-то ЯП x += n определено как x = n + x?

Например, в соответствии со стандартом C++ неважно, как определён оператор += — компилятор волен переставить побочные эффекты между точками следования как ему угодно, так что может получиться и 6, и 7, и что-нибудь ещё.
Тогда зачем вообще придумывать эти операторы, если ими потом нельзя пользоваться?
Мне не понятно зачем ими пользоваться в таком ключе, зачем они были придуманы я вполне понимаю.
Кстати, а x += n в C# во всех контекстах эквивалентно x = x + n?
В Java, например, тип результата приводится к типу x.
VS2010 — ответ 6. Не поленился запустить запылившуюся студию на компе)
А по сути верно, не надо такой код писать.
Неопределённое поведение же. Переменная изменяется несколько раз в одной точке следования. Хабр не торт
В C# — вполне определенное. Это вам не C.
А вот что говорит мой Сишный компилятор:
Warning[Pa079]: undefined behavior: variable «x» (declared at line 25) (or a value reached by some form of indirection through it) is modified more than once without an intervening sequence point in this D:\work\HabarHabar\main.c 26
Если я не ошибаюсь, то про это я читал еще в одной из первых своих книг Begin C# года так 2008. Да и не раз потом встречал про разницу между x++ и ++x.
Не увидел не чего не обычного и нового.
Дело не в разнице, а в том, что в разных компиляторах, в одном компиляторе на разных настройках, и даже на одинаковых настройках в разных версиях компилятора эта конструкция может вести себя по разному. Вчера было 6, сегодня 7, а через 10 лет ваш квантовый компьютер взорвётся.
Ну знаете, компилятор такая вещь.
С таким же успехом можно писать, что var это плохо, а вместо ключевого int нужно писать конкретно Int32, вдруг они поменяют на Int64.
Мало ли что будет через 10 лет.
Не может. В отличие от C и C++, порядок выполнения подобных выражений определён стандартом языка C#.
Да, похоже, читая топик, я так и не заметил, что речь идёт не о C/C++ )
Выбрал 7 (для C), потом решил проверить:

20:33:19 {ruzin@MacBook-Ruzin}$~> cat z.c

#include <stdio.h>

int main() {
	int x = 3;
	x += x++;

	printf("%i\n", x);
	return 0;
}


(kit)20:32:54 {ruzin@MacBook-Ruzin}$~> cc z.c
(kit)20:32:58 {ruzin@MacBook-Ruzin}$~> ./a.out
7

=========
в С# не проверял.

Не пойму, почему x += n должно разворачиваться компилятором в x = x + n?

В ASM вроде есть добавления чего-то к чему-то уже существующему и сама операция += как раз сделана, чтобы минимизировать количество команд ASM.
=========

(kit)20:40:16 {ruzin@MacBook-Ruzin}$~> CFLAGS="-g -O0" make z
cc -g -O0 z.c -o z

(kit)20:40:49 {ruzin@MacBook-Ruzin}$~> gdb z
GNU gdb 6.3.50-20050815 (Apple version gdb-1822) (Sun Aug 5 03:00:42 UTC 2012)
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type «show copying» to see the conditions.
There is absolutely no warranty for GDB. Type «show warranty» for details.
This GDB was configured as «x86_64-apple-darwin»...Reading symbols for shared libraries… done

(gdb) break main
Breakpoint 1 at 0x100000f16: file z.c, line 4.
(gdb) run
Starting program: /Users/ruzin/z
Reading symbols for shared libraries +… done

Breakpoint 1, main () at z.c:4
4 int x = 3;

(gdb) disassemble
Dump of assembler code for function main:
0x0000000100000f00 <main+0>:	push   %rbp
0x0000000100000f01 <main+1>:	mov    %rsp,%rbp


0x0000000100000f04 <main+4>:	sub    $0x10,%rsp
0x0000000100000f08 <main+8>:	lea    0x5f(%rip),%rdi        # 0x100000f6e

0x0000000100000f0f <main+15>:	movl   $0x0,-0x4(%rbp)
0x0000000100000f16 <main+22>:	movl   $0x3,-0x8(%rbp)        # x = 3
0x0000000100000f1d <main+29>:	mov    -0x8(%rbp),%eax        # x (3) -> register EAX (3)
0x0000000100000f20 <main+32>:	mov    %eax,%ecx              # register EAX (3) -> register ECX (3)
0x0000000100000f22 <main+34>:	add    $0x1,%ecx              # register ECX (3) += 1 // register ECX (4)
0x0000000100000f28 <main+40>:	mov    %ecx,-0x8(%rbp)        # register ECX (4) -> x (4)
0x0000000100000f2b <main+43>:	mov    -0x8(%rbp),%ecx        # x (4) -> register ECX (4)
0x0000000100000f2e <main+46>:	add    %eax,%ecx              # register ECX (4) += register EAX (3) // register ECX (7)
0x0000000100000f30 <main+48>:	mov    %ecx,-0x8(%rbp)        # register ECX (7) -> x (7)
0x0000000100000f33 <main+51>:	mov    -0x8(%rbp),%esi
0x0000000100000f36 <main+54>:	mov    $0x0,%al
0x0000000100000f38 <main+56>:	callq  0x100000f4e <dyld_stub_printf>
0x0000000100000f3d <main+61>:	mov    $0x0,%ecx
0x0000000100000f42 <main+66>:	mov    %eax,-0xc(%rbp)
0x0000000100000f45 <main+69>:	mov    %ecx,%eax
0x0000000100000f47 <main+71>:	add    $0x10,%rsp
0x0000000100000f4b <main+75>:	pop    %rbp
0x0000000100000f4c <main+76>:	retq

End of assembler dump.
(gdb)
Обратите внимание: в статье идет речь только о C#. Зачем вы все отвечаете про С/С++?

C# компилируется в байт-код. И оператор x +=… компилируется в последовательность «загрузить x — вычислить то, что справа — сложить — записать x». Что в этой последовательности нелогично?
Автор уж слишком ненавязчиво указал, что речь про C#
Я не знал почему почему в C# x += x++ разворачивается в x = x + x++. Ниже ответили, это и объясняет почему в результате 6.

Мне также показалось, что сравнение с C/C++ будет интересным, да и на Mac у меня нет C#, чтобы проверить :)

В данном случае это ошибка компилятора C++. точнее то, что называется «неопределённое поведение»
x = x + x++;
такой код тоже даст в результате 7, и это неправильно. Хотя неправильнее писать такой код.
Не пойму, почему x += n должно разворачиваться компилятором в x = x + n?


В языке С# тип int — синоним структуры Int32, которая является потомком Object. При его использовании применяются все те-же правила, которые работают для любых типов с перегруженными операторами.
В языке C# невозможно перегрузить операторы +=, -=, /=, и *=. Они всегда разворачиваются в соответствующие операторы +, -, *, / c присвоением. Присвоение невозможно перегрузить. Операторы инкремента перегрузить можно, но они обязаны создавать копию объекта и менять уже её, а не модифицировать состояние исходного.

Внутри CLR работает стековая машина.

Выражение i++ транслируется в:
load i
duplicate
call operator++
store i
На стеке остается исходное значение i.

Выражение ++i транслируется в:
load i
call operator++
duplicate
store i
На стеке остается измененное значение i.

Соответственно в языке С# невозможно перегрузить постфиксный и префиксный операторы инкремента отдельно.

Выражение i+=expr транслируется в:
load i
<код expr>
call operator +
store i

Сложив два кусочка кода и выполним в уме.
Получается 6.
Все на много легче:

Рассмотрим еще раз код
int x = 3;
x = x + x++;


Теперь представим код вот так:
Piccy.info - Free Image Hosting

Так, если компилятор разбирает выражения слева на право, то первое что сделает компилятор, это посчитает левый узел сохранит его значение в переменную temp1, потом посчитает правый узел сохранит его значение в переменную temp2, а потом сложит
 int x = temp1 + temp2; 
Вот что значит — правильный выбор абстракций в рассуждениях :)
Разве x++ в таком случае опустится?
На самом деле, понятно почему это «не определенное поведение». Так как все зависит от того, считается все слева на право или справа на лево. Ваша абстракция это наглядно доказывает.
А в чем неопределенность-то? Как ни разбирай — приоритет операций должен быть инвариантным.
И да, более подробно AST будет выглядеть так:

     +
X         X
             ++

(++ находится в поддереве правого X). По семантике постинкремента значение правого X будет использовано вышестоящим + до применения инкремента.
Пардон, а какая разница как оно разворачивается?
если сначала выполняется условие x++, то имеем выражение x+=x0 (3 += 4) и ответ получается 7
если после, то x+=x => 3+3 после чего выполнится x++, что опять-таки повлечет за собой в ответе 7
вы же в конце концов выводите х уже после всех операций. Если ваш компилятор выводит 6, это означает, что пропустил одну из инструкций. Вот и все.
x+=x => 3+3 после чего выполнится x++
Присвоение выполняется самым последним, а выражения вычисляются слева направо. Так что x++ будет выполнено раньше присвоения, но его результат будет затёрт. Если б в выражении правее инкремента стоял бы ещё один x, то его значение было б уже 4.
Немного ликбеза:
Бинарные выражения большинством компиляторов/интерпретаторов выполняются так.
1) выбираем оператор с наименьшим приоритетом
2) вычисляем его слева-направо для всех операторов кроме присвоения.
вычисляем первый аргумент, вычисляем второй аргумент, выполняем первую операцию, вычисляем третий аргумент, выполняем вторую операцию и т.д.
Присвоение для понимания лучше преобразовать в выражение со скобками. Так a+=b раскрывается как a = (a+b).
a+=b=c+=d раскрывается так a = (a+(b=c+=d)), a = (a+(b=(c+=d))), a = (a+(b=(c=(c+d))))
Унарные операторы вычисляются когда вычисляется непосредственно аргумент в который они входят. Их можно раскрыть с помощью функции (х (в компиляторе используется инструкция дублирования значения на вершине стека)
Например:
x++;
T RightIncrement (ref T x) {
var tmpx = x;
x = x+1;
return tmpx;
}

++x; раскрывается как (x=x+1)

Тогда наше выражение раскрывается как
x = (x + RightIncrement(ref x))
Код сформированный компилятором:

IL_0003: ldloc.0 //вычисляем x. стек [x]
IL_0004: ldloc.0 // начинаем вычислять x++. Загружаем в стек x. стек [x,x]
IL_0005: dup //сохраняем в стеке x и загружаем его копию. стек [x,x,x]
IL_0006: ldc.i4.1 //загружаем 1. стек [x,x,x,1]
IL_0007: add //выполняем сложение. стек [x,x,(x+1)]
IL_0008: stloc.0 //устанавливаем новое значение x. Обратите внимание что в стеке находится 2 СТАРЫХ значения x. стек [oldx,oldx]
IL_0009: add // выполняем операцию сложения. В стеке [oldx+oldx]
IL_000a: stloc.0 //выполняем оператор присвоения, стек пуст. В переменной x находится сумма двух его первоначальных значений

P.S. Рекомендую обратить внимание на то, что не всегда выполняются все аргументы оператора. Например логические операторы выполняются до момента когда будет известно значение всего выражения. Также в ходе выполнения операции может возникнуть исключение, тогда часть действий выполнится, а часть нет.
Меня такой код совершенно с ума не сводит, очень простой. Но я бы не рекомендовал так писать. Вокруг очень много программистов, которые не знают язык, на котором пишут.
Как в Руби все запущенно… Даже варианта ответа такого нет.

$ irb
2.0.0-p247 :001 > x=3
 => 3 
2.0.0-p247 :002 > x += x++
2.0.0-p247 :003 >   x
 => 9 
Простите, был взволнован, когда писал. Оказывается я выражение не завершил.
Sign up to leave a comment.

Articles