19 марта 2010 в 18:55

Все знают что "++i + ++i" — плохо, но что-же за ширмой?

Баг, выглядывающий из-за ширмы на вас о_ОНесомненно, все программисты знают что использование выражений, подобных тому что приведено в заглавии поста, не то что нежелательно, а строго противопоказано. Такие конструкции, поведение компилятора в которых не определено, могут принести множество трудноуловимых ошибок и нежелательных последствий. Но уверен, многим начинающим программистам хотелось бы по глубже понять эту проблему и, заглянув за ширму компилятора, узнать что же именно происходит в таких случаях. Изучению одного из примеров подобного кода я и посвящаю этот пост. Добро пожаловать под кат :)

Объект исследования


Для примера, я разберу работу выражения "++i + ++i" на двух различных языках: С и С#. Первый, как известно, компилируется в нативный код процессора, а второй, если грубо, работает на основе виртуальной стековой машины. И так, рассмотрим сами примеры:

Исходник на Си:
  1. #include <stdio.h>
  2.  
  3. void main()
  4. {
  5.  int i = 5;
  6.  i = ++i + ++i;
  7.  printf("%d\n",i);
  8. }

И исходник на C#:
  1. using System;
  2.  
  3. public class Test
  4. {
  5.  public static void Main()
  6.  {
  7.   int i = 5;
  8.   i = ++i + ++i;
  9.   Console.WriteLine(i);
  10.  }
  11. }

За ширмой Си


Воспользовавшись дизассемблером, посмотрим что-то же в итоге сгенерировал компилятор Си:

#A#5:  int i = 5;                        
  cs:0295 BE0500         mov    si,0005  
#A#6:  i = ++i + ++i;                    
  cs:0298 46             inc    si       
  cs:0299 46             inc    si       
  cs:029A 8BC6           mov    ax,si    
  cs:029C 03C6           add    ax,si    
  cs:029E 8BF0           mov    si,ax    
#A#7:  printf("%d\n",i);                 
  cs:02A0 56             push   si       
  cs:02A1 B8AA00         mov    ax,00AA  
  cs:02A4 50             push   ax       
  cs:02A5 E8330C         call   _printf  

Как видно из листинга, компилятор отобразил переменную i на регистр SI процессора x86. После чего, дважды инкрементировав этот регистр, прибавил его самого к себе через аккумулятор AX. В результате, переменная i становится равной 14.

За ширмой C#


С помощью Ildasm посмотрим что скрывается за C#:

.method public hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       21 (0x15)
  .maxstack  3
  .locals init (int32 V_0)
  IL_0000:  ldc.i4.5    // push 5                       5
  IL_0001:  stloc.0     // i := pop()                   null
  IL_0002:  ldloc.0     // push i                       i
  IL_0003:  ldc.i4.1    // push 1                       i, 1
  IL_0004:  add         // push (pop() + pop())         (i+1) т.е. 6
  IL_0005:  dup         // copy вершины стека           6, 6
  IL_0006:  stloc.0     // i := pop() // i := 6         6
  IL_0007:  ldloc.0     // push i                       6, i
  IL_0008:  ldc.i4.1    // push 1                       6, i, 1
  IL_0009:  add         // push (pop() + pop())         6, (i+1) т.е. 7
  IL_000a:  dup         // copy вершины стека           6, 7, 7
  IL_000b:  stloc.0     // i := pop() // i := 7         6, 7
  IL_000c:  add         // push (pop() + pop())         13
  IL_000d:  stloc.0     // i := pop()                   null
  IL_000e:  ldloc.0     // push i                       i
  IL_000f:  call       void [mscorlib]System.Console::WriteLine(int32)
  IL_0014:  ret
} // end of method Test::Main

Для наглядности, я добавил комментарии к ассемблеру виртуальной стековой машины. Смотря на листинг, можно увидеть что для инкремента переменной i в стек помещается сама переменная и единица. Затем выполняется команда сложения которая, взяв два значения из стека и сложив, помещает результат опять в стек. Затем происходит дублирование вершины стека и запись значения обратно в переменную. Таким образом в стеке остается 5 + 1, т.е. 6. Далее цикл повторяется для другого инкремена: заносится в стек переменная, вслед за ней — единица, происходит сложение, дублирование вершины и запись результата второго инкремента назад в переменную. Теперь уже в i будет 7, а в стеке останутся 6 от первого случая и 7 от второго. Затем выполняется команда сложения и результат, равный теперь 13, заносится в переменную.

Итоги


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

Если тема вам показалась интересной — дайте знать, и я напишу еще пару занятных моментов из мира компиляции ;)

UPD. В комментариях подсказывают что в РНР, Java, Actionscript 3, JavaScript и TCL результат тоже равен 13, а вот в Perl, K++ и C GCC — 14 (но результат GCC может зависеть от настроек оптимизатора) :)

Отличились PL/SQL и Python — 10 (как подсказывает К.О. — из-за отсутствия инкрементов в языке), однако Bash — 12.

Так же есть компиляторы, непозволяющие написание подобного кода. Например Ruby (или вот так).

А каким будет результат в твоем, %username%, компиляторе?

UPD2. Ссылки по теме: точки следования Википедия, Алёна C++.
Лабинский Николай @Labinskiy
карма
125,0
рейтинг 0,0
Самое читаемое Разработка

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

  • +9
    Спрашивайте, критикуйте… :)
    • 0
      >> i = ++i + ++i

      на собеседованиях подобные примеры любят задавать. Вот в следующий раз дам ссылку им на эту статью, может быть тогда поймут, и будут просто спрашивать просто, какие у инкремента и декремента.
      • 0
        вот я наговорил… Правильно: будут спрашивать, какие отличия у инкремента и декремента
        • +7
          а-а-а, убейте меня, надо же 2 раза подряд ошибиться :(

          Преинкремент и постинкремент.
          • +1
            на собеседованиях спрашивают применительно к конкретному языку
    • +2
      Надо иголки под ногти кто так в реальных проектах пишет.

      ЗЫ %)
  • +7
    Пишите — такие вещи всегда интерестны для тех, кому лень разбираться «как всё работает» на самом деле изнутри :). А информация очень полезна для них.
  • НЛО прилетело и опубликовало эту надпись здесь
    • +1
      Моя Javascript сказал 13, а питон — таки да, 10… Теперь они не захотят больше работать вместе)
  • +2
    Напомнило как 3 года назад мы разбирали случай ++i + i++
    gameworld.com.ua/showthread.php/161-disasm-vs-C-disasm

    а вообще таких конструкций в коде я стараюсь избегать
  • +4
    На Java 13.

    Но проблема немного надуманная и очень редко встречается с реальных проектах, а если и встречается, то легко обходится другими способами.
  • +1
    Аж передергивает, как вспомню этот чертов ++i + ++i с университета.
    • +5
      Главное — это сразу забыть! :)
  • +10
    никогда бы не подумал так извращаться с инкрементированием.
  • +8
    Головная боль от таких выражений кончается после прочтения стандарта и уяснения что такое sequence points (кажется глава 5 в стандарте C++). Копать асм занятно, но не имеет особого смысла, стандарт дает это на откуп создателям компилятора.
    • –1
      Спасибо за наводку, добавил ссылку на Википедию.
  • +1
    С скомпиленный в gcc:
    14
  • +5
    Изящно проблема решена в Ruby :)
    Ruby has no pre/post increment/decrement operator. For instance, x++ or x-- will fail to parse. More importantly, ++x or --x will do nothing! In fact, they behave as multiple unary prefix operators: -x == ---x == -----x ==… To increment a number, simply write x += 1.
    • +10
      Нет оператора — нет проблемы :)

      Отличное решение
      • +1
        Мне в этом плане больше импонирует Хаскель. Нет переменных — нет проблем.
        Да и отладчик там Хаскеле не нужен, не говоря о дизассемблере.
    • 0
      в руби эта проблема решена созданием методов times, upto, downto, each
      необходимость в инкременте/декременте просто отпадает
      • +1
        Инкремент/декремент нужен не только в циклах :)
        • 0
          само собой)
          но изменение счетчика цикла — один из наиболее популярных случаев его использования
    • 0
      x += 1 работает и в Си и в Яве. Но он не возвращает значение. К примеру, написать a = x++ можно, а написать a = x += 1 — нельзя.
      • +2
        В перле можно :)
      • +2
        Не уверен про java, но в си, конечно же, возвращает :) и a = x += 1 — вполне законная конструкция
        • 0
          Каюсь, не знал :) Однако ни в Руби, ни в Пайтоне такого всё равно делать нельзя, увы :)
          • 0
            Очень интересно. С чего это вы взяли, будто a = x += 1 в Руби вне закона? Очень даже в.
          • 0
            irb(main):001:0> a=0
            => 0
            irb(main):002:0> x=1
            => 1
            irb(main):003:0> a = x +=1
            => 2
            irb(main):004:0> a
            => 2
            irb(main):005:0> x=1
            => 1
            irb(main):006:0> a=0
            => 0
            irb(main):007:0> a = x +=1 + x +=1
            => 4

            p.s.
            на лурке про это статья есть
          • 0
            цепное присвоение, на сколько мне известно, доступно почти во всех языках
    • 0
      Питон себя ведет абсолютно также. Так что автору стоит подправить UPD.
  • +2
    В gcc подобное выражение может принимать разные значения в зависимости от разных параметров (например при -O2 и -O3 может быть разный ответ). Штука кстати известная, соль её в том что нельзя изменять параметр более одного раза между двумя точками следования.
    • 0
      Я думаю что использование volatile тоже может повлиять ;)
  • +5
    Ха, а в с++ просто в стандарте сказанно, что модификация переменной между двумя точками следования есть undefined behavior, поэтому смотрение ассемблерного кода — становиться бессмысленным.

    • +2
      Но ведь интересно же! :)
    • 0
      Вроде бы не undefined, а unspecified. То есть, ничего страшного произойти не должно — компилятор всего лишь может по-разному интерпретировать код.
  • +1
    python — 10
    — #!/usr/bin/env python

    i = 5
    i = ++i + ++i
    print i
    • +6
      потому что в пейтоне нет инкремента
      --Кэп
    • –1
      i = 5
      i = +--+++--+i +--+++--+i
      print i

      выводит 10!
    • 0
      в данном случае, вместо инкремента или декремента происходит установка знака у числа. //KO
  • +2
    Если у вас будет время, напишите пожалуйста еще подобных статей. Было интересно)
    • –1
      Спасибо, постараюсь ;)
  • НЛО прилетело и опубликовало эту надпись здесь
    • 0
      а что поганого? Правильный ответ ведь есть. Он не в числовой форме, но есть.
      • НЛО прилетело и опубликовало эту надпись здесь
        • 0
          Правильный ответ всё-таки — поведение неопределено :). Про «я бы увольнял» ответить может кандидат на должность, которая предполагает, что он будет наделён полномочиями увольнения сотрудников, но для такой вакансии это более чем странный вопрос :).
          А HR же откуда-то получает эту бумажку? Наверное, от технических специалистов этой фирмы. Причём не самого нижнего уровня. А, скорее всего, от будущего непосредственного начальника этого кандидата или сотрудника того же уровня, что вакансия. И если начальник или признаный технический специалист этой конторы считает, что там именно 12-13-14, то это ведь такой своеобразный тест на адекватность этого начальника и конторы, не правда ли? :) Так что всё в порядке с этим тестом :).
          • НЛО прилетело и опубликовало эту надпись здесь
            • 0
              Да, я именно это и имел в виду под тестом на адекватность.
  • +1
    В TCL нет оператора ++i, но есть аналог i++. Результат вполне логичен:
    set i 5
    puts [expr [incr i]+[incr i]]

    13
  • +1
    Спасибо! Пиши ещё!=)
  • –1
    В копилку информации, аналогично на PL/SQL в Oracle10:
    declare
     i number;
    begin
     i:=5;
     dbms_output.put_line(i++ + i++ );
    end;



    Результат 10. Но только потому, что в PL/SQL нет инкремента. Странно что на синтаксис не ругается. На i++ ругается, т.к. ожидалось увидеть второе слагаемое.
    • 0
      В предыдущем комменте должны были быть ++i, но случайно захватил исправленный блок кода.
    • 0
      А можно, ради чистоты эксперимента, тоже самое но с префиксной записью?
      • +2
        declare
        i number;
        begin
        i:=5;
        dbms_output.put_line(++i + ++i);
        end;



        Результат 10. Повторюсь, в PL/SQL нет инкремента — это результат сложения.
        • 0
          Не совсем в тему, но мне показалось удивительным (раз уж разговор зашел про СУБД):

          mysql> select -------------1 xx;
          +----+
          | xx |
          +----+
          | -1 |
          +----+
          1 row in set (0.00 sec)
          • 0
            Нечетное число (в вашем примере 13) унарных минусов, что вас смущает?
            • –1
              А то, что --, как и в Oracle, обозначает начало однострочного комментария.
              Попробуйте, например, mysql> select ------- ------1 xx;
              • 0
                Это надо в БНФ нотацию языка смотреть и токенайзер.
              • +3
                В MySQL комментарий, о котором вы говорите, это не просто два минуса, а еще и символ пробела за ним — "-- ".
  • +1
    perl: 13
    #!/usr/bin/perl -w
    my $i=5;
    $i = ++$i + $i++;
    print $i;
    • 0
      А можно тоже, но с двумя префиксными инкрементами как в посте?
      • +1
        14 получится, только что проверил
        • 0
          Спасибо, обновил цифру в посте.
        • +1
          Да, 14
  • –2
    интересно, кто такой говнокод применяет? даже для си все зависит от компилятора
  • +2
    А каким будет результат в твоем, %username%, компиляторе?

    $ kpp -e test.kpp
    14

    $ cat test.kpp
    export function main() {
       var i = 5;
       printf("%d\n", ++i + ++i);
    }

    $ cat test.gide
    use "gbc:diss:/lib/kpp.gbc"

    const $$0, std/int, "5"
    const $$1, std/string, "%d\n"

    function main
       new $0, std/int
       call $0, =, $$0
       mov i$0, $0

       call i$0, ++=
       mov $0, @result
       call i$0, ++=
       call $0, +, @result
       mov $1, @result
       call 0, kpp/printf, $$1, $1
    end


    P.S: Можно было бы еще написать про то, что если строка записана как «++i+++i», то приоритет операторов тоже может оказать свое влияние.
    • +1
      Простите, а как называется этот язык? )
    • +1
      мой компилятор такое не позволяет писать
      www.isdelphidead.com
  • +2
    42
    • 0
      тогда уж и 4 8 15 16 23 42 ;)
  • 0
    var i:Number = 5;
    i = ++i + ++i;
    trace(i); // результат 13
    Actionscript 3 (MXMLC/flex 4) flashplayer 10.
  • +1
    Спасибо за статью. В следующий раз хотелось бы чего-то более прикладного.
  • +1
    javascript:

    var i = 5;
    i = ++i + ++i;
    alert(i);


    результат: 13
  • +3
    bash — 12.
    если мыслить логически то 13 правильный ответ.
    • +2
      Все зависит от языка. Например, если мне не изменяет память (я могу ошибаться), в С первыми выполняются унарные операции, то есть инкремент в данном случае. И логичным выглядит ответ 14 (два раза увеличить и сложить).
  • НЛО прилетело и опубликовало эту надпись здесь
  • +1
    Считаю неправильно жертвовать супероптимизаций в ущерб читаемости кода.
    Кроме того желательно, чтобы код алгоритмов при переносе не только между компиляторами, но и похожими языками выдавал один и тот же прогнозируемый результат.
  • +1
    На руби можно попробовать так:

    irb(main):001:0> i = 5
    => 5
    irb(main):002:0> (i += 1) + (i += 1)
    => 13
  • 0
    Обработка в браузерах
    i = 0;
    res = ++i + ++i + ++i + ++i; //4 раза

    Chrome, Firefox, IE8, Safari: 10
    Opera (по крайней мере 11.01 b1190): 32
    И это явно глюк, т.к. при сумме 3-х инкрементов Опера дает аналогично другим браузерам 6, а выше — начинает выдавать странное.
    • 0
      >Обработка в браузерах
      в смысле клиентский JavaScript

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