Все знают что "++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++.
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 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
                                                Если у вас будет время, напишите пожалуйста еще подобных статей. Было интересно)
                                              • НЛО прилетело и опубликовало эту надпись здесь
                                                • 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 получится, только что проверил
                                                          • –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», то приоритет операторов тоже может оказать свое влияние.
                                                            • +2
                                                              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

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