Пользователь
0,0
рейтинг
5 августа 2013 в 00:00

Разработка → x += x++ перевод

C#*, .NET*
Хотел бы начать перевод с маленького опроса. Вопрос к .NET разработчикам пишущим на языке программирования C#.

Опрос в конце перевода.

Сегодня я смотрел внутренний список разработчиков языка C#. Один из вопросов был о поведении выражения «x += x++», а именно, каким должно быть правильное поведение. Я думаю этот вопрос более чем интересный, поэтому решил посвятить ему запись в своем блоге. Но для начала, НИКОГДА НЕ ПИШИТЕ ТАКОЙ КОД.

ОК, мы можем начать…

Используем следующий код для примера:

int x = 3;
x += x++;

Первое, что делает компилятор, когда видит код z += y это преобразует его в z = z + y. Это, очевидно верно, для операторов +=, -=, *=, /=.

ОК, это было просто. Теперь, мы можем просто посчитать значение такого выражения:

x = x + x++;

Этот код дает такой же результат, что и код:

x = x + x;

Однако, он дает различный результат с кодом:

x = x++ + x;

который в свою очередь дает одинаковый результат с кодом:

x = x + ++x;

Как бы такое поведение не сводило вас с ума, оно действительно имеет место быть. Но для начала ответим на вопрос: какая разница между x++ и ++x? x++ возвращает значение x текущему выражению, а затем увеличивает его на единицу. ++x увеличивает значение x на единицу и затем возвращает увеличенное значение текущему выражению. Принимая во внимание вышесказанное (и учитывая, что C# обрабатывает выражения слева на право), мы можем понять, почему предыдущий код работает именно так.

int x = 3;
x = x + x++;

Компилятор будет подсчитывать это выражение следующим образом:

  1. x = (x) + x++ -> первый x подсчитывается и возвращает значение 3, x = 3;
  2. x = 3 + (x)++ -> x подсчитывается и возвращает 3, x = 3;
  3. x = 3 + (x++) -> x++ возвращает значение 3 и x увеличивается на единицу, x = 4;
  4. x = (3 + 3) -> выражение 3 + 3 подсчитывается и возвращает 6, x = 4;
  5. (x = 6) -> x присваивается значение 6 (переписывая предыдущее значение 4).

Теперь посмотрим, что будет в результате такого кода:

int x = 3;
x = x++ + x;

  1. x = (x)++ + x -> x подсчитывается и возвращает значение 3, x = 3;
  2. x = (x++) + x -> x++ возвращает значение 3 и x увеличивается на единицу, x = 4;
  3. x = 3 + (x) -> x подсчитывается и возвращает значение 4, x = 4;
  4. x = 3 + 4 -> выражение 3 + 4 подсчитывается и возвращает значение 7, x = 4;
  5. (x = 7) -> x присваивается значение 7 (переписывая предыдущее значение 4).

Теперь рассмотрим такой код:

int x = 3;
x = x + ++x;

  1. x = (x) + ++x -> первый x подсчитывается и возвращает значение 3, x = 3;
  2. x = 3 + (++x) -> ++x увеличивает x на единицу и возвращает 4, x = 4;
  3. x = 3 + (x) -> x подсчитывается и возвращает 4, x = 4;
  4. x = 3 + 4 -> выражение 3 + 4 подсчитывается и возвращает 7, x = 4;
  5. (x = 7) -> x присваивается значение 7 (переписывая предыдущее значение 4).

Я надеюсь, теперь все ясно. Кстати, в языке С++ поведение такого выражения не определено.

Так почему же мы определили поведение такого выражения? Почему мы не говорим об ошибке или почему не происходит предупреждения во время компиляции?

  • Мы были неправы, мы должны получить ошибку или предупреждение, но теперь уже слишком поздно, потому что если мы изменим это поведение, то можем сломать код; ИЛИ
  • Довольно сложно, сформировать набор руководящих правил для компилятора, чтобы он был способен сказать об ошибке в таких странных случаях; ИЛИ
  • Мы предпочитаем тратить наше время, работая над вещами, которые действительно заботят людей вместо сглаживания таких острых углов.

Имеет ли значение, какой из предыдущих вариантов является правильным? Не совсем, потому что ВЫ НЕ ДОЛЖНЫ ПИСАТЬ ТАКОЙ КОД!
Чему будет равно значение переменной x после выполнения следующего кода int x = 3; x += x++;?

Проголосовало 1438 человек. Воздержалось 355 человек.

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

Перевод: Luca Bolognese
Гуев Тимур @timyrik20
карма
111,7
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (48)

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

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

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

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

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

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

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

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

      Например, в соответствии со стандартом C++ неважно, как определён оператор += — компилятор волен переставить побочные эффекты между точками следования как ему угодно, так что может получиться и 6, и 7, и что-нибудь ещё.
    • +1
      <?php $x = 3; $x += $x++; echo $x;

      =7 :).
      • 0
        Тогда зачем вообще придумывать эти операторы, если ими потом нельзя пользоваться?
        • +1
          Мне не понятно зачем ими пользоваться в таком ключе, зачем они были придуманы я вполне понимаю.
    • 0
      Кстати, а x += n в C# во всех контекстах эквивалентно x = x + n?
      В Java, например, тип результата приводится к типу x.
  • –4
    VS2010 — ответ 6. Не поленился запустить запылившуюся студию на компе)
    А по сути верно, не надо такой код писать.
  • 0
    Аналогичный пост на хабре про ++i + ++i
  • –1
    Неопределённое поведение же. Переменная изменяется несколько раз в одной точке следования. Хабр не торт
    • +5
      В C# — вполне определенное. Это вам не C.
  • +1
    А вот что говорит мой Сишный компилятор:
    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
  • 0
    Если я не ошибаюсь, то про это я читал еще в одной из первых своих книг Begin C# года так 2008. Да и не раз потом встречал про разницу между x++ и ++x.
    Не увидел не чего не обычного и нового.
    • 0
      Дело не в разнице, а в том, что в разных компиляторах, в одном компиляторе на разных настройках, и даже на одинаковых настройках в разных версиях компилятора эта конструкция может вести себя по разному. Вчера было 6, сегодня 7, а через 10 лет ваш квантовый компьютер взорвётся.
      • 0
        Ну знаете, компилятор такая вещь.
        С таким же успехом можно писать, что var это плохо, а вместо ключевого int нужно писать конкретно Int32, вдруг они поменяют на Int64.
        Мало ли что будет через 10 лет.
      • 0
        Не может. В отличие от C и C++, порядок выполнения подобных выражений определён стандартом языка C#.
        • 0
          Да, похоже, читая топик, я так и не заметил, что речь идёт не о C/C++ )
  • 0
    Выбрал 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)
    • +2
      Обратите внимание: в статье идет речь только о C#. Зачем вы все отвечаете про С/С++?

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

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

    • 0
      В данном случае это ошибка компилятора C++. точнее то, что называется «неопределённое поведение»
      x = x + x++;
      такой код тоже даст в результате 7, и это неправильно. Хотя неправильнее писать такой код.
    • 0
      Не пойму, почему 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.
  • +3
    Все на много легче:

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


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

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

             +
        X         X
                     ++
        

        (++ находится в поддереве правого X). По семантике постинкремента значение правого X будет использовано вышестоящим + до применения инкремента.
  • –1
    Пардон, а какая разница как оно разворачивается?
    если сначала выполняется условие x++, то имеем выражение x+=x0 (3 += 4) и ответ получается 7
    если после, то x+=x => 3+3 после чего выполнится x++, что опять-таки повлечет за собой в ответе 7
    вы же в конце концов выводите х уже после всех операций. Если ваш компилятор выводит 6, это означает, что пропустил одну из инструкций. Вот и все.
    • +1
      x+=x => 3+3 после чего выполнится x++
      Присвоение выполняется самым последним, а выражения вычисляются слева направо. Так что x++ будет выполнено раньше присвоения, но его результат будет затёрт. Если б в выражении правее инкремента стоял бы ещё один x, то его значение было б уже 4.
  • 0
    Немного ликбеза:
    Бинарные выражения большинством компиляторов/интерпретаторов выполняются так.
    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))
    • 0
      Код сформированный компилятором:

      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. Рекомендую обратить внимание на то, что не всегда выполняются все аргументы оператора. Например логические операторы выполняются до момента когда будет известно значение всего выражения. Также в ходе выполнения операции может возникнуть исключение, тогда часть действий выполнится, а часть нет.
  • 0
    Меня такой код совершенно с ума не сводит, очень простой. Но я бы не рекомендовал так писать. Вокруг очень много программистов, которые не знают язык, на котором пишут.
  • 0
    Как в Руби все запущенно… Даже варианта ответа такого нет.

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

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