++i + ++i
A long time ago, in a galaxy far, far away...
События и явления, описанные в этой статье, были давно, и помнит о них разве что пара-другая олдфагов. Но Анонимус не забывает!
Обсуждение этой статьи неиллюзорно доставляет не хуже самой статьи. Рекомендуем ознакомиться и причаститься, а то и поучаствовать, иначе впечатление будет неполным.
int i = 5;
i = ++i + ++i;
— типичный код-майндфак для программистов.
Строгое описание происходящего
В данном примере происходит неоднократное изменение переменной в пределах одной точки следования, такая ситуация описывается в стандартах C и С++ как UB. Иными словами, даже попытки ответить на этот вопрос иначе как «UB» демонстрируют недостаточную квалификацию отвечающего. Другое дело, что после «UB» можно указать некоторые подробности, и мы этим займёмся, поскольку не утоленное вовремя любопытство приводит к драмам в обсуждении.
Конкретно неопределённость в этой, как некоторым кажется, кристально ясной конструкции в данном случае заключается в том, что, согласно стандартам С и С++, побочные эффекты (то есть инкремент в данном случае) могут быть применены в любой удобный для компилятора момент между двумя точками следования. Конструкцию i = ++i + ++i;
компилятор вправе понять и как
tmp=i; tmp++; i = tmp; tmp++; i += tmp;
и как
tmp=i; tmp++; tmp++; i = tmp + tmp;
и какими-нибудь другими способами. Нужна такая свобода для проведения низкоуровневых оптимизаций в обычных случаях типа a = ++b + ++c;
, дабы между делом сэкономить пару тактов на халяву.
Хотя, оптимизатор тут вообще ни при чём. Дело в том, что наше интуитивное понимание работы этого кода основывается на том, что прединкремент возвращает значение, получившееся после прибавления единицы. На самом же деле любой нормальный прединкремент возвращает не получившееся значение, а ссылку на эту же переменную. Поэтому мы складываем не числа, а две одинаковые ссылки, то есть переменную i
саму с собой! Иными словами происходит буквально следующее:
- Левый
++i
прибавляет единицу к i и возвращает ссылку на неё. I = 6. - Правый
++i
прибавляет ещё одну единицу к i и также возвращает ссылку на неё. I = 7. - Оператор сложения разыменовывает ссылки, получая
i = i + i
. Так как после второго шага I = 7, то извлекается именно это число, давая выражениеi = 7 + 7
, откуда и получается 14.
Нельзя, но если всё-таки сделаем?
Почему нельзя, мы уже видели (даже наиболее «альтернативно одаренные» не возьмутся оспаривать, что это ведет ко глюку). Однако всякий пытливый ум, несомненно, изучит этот глюк в поисках того, как именно может сглючить данный код, в какую сторону стандарты языка допускают маневры компилятора, а в какую — таки нет.
Варианты «правильного» ответа колеблются между 13 и 14, хотя на LISPе (пруф) анонимусы вроде как получили 12, 13 (пруф), а на Питоне вообще 10 (поскольку там вообще нет оператора инкремента ++). Результат зависит от последовательности операций. Там, где результат 13, сначала вычисляется первый инкремент, на место первого операнда идёт 6, вычисляется второй инкремент, туда идет 7, и результаты складываются: 6+7=13. 14 можно получить, вычислив оба инкремента, а потом уже сумму. Таким образом, этот трюк хорошо использовать для ошеломления быдлокодеров, уверенных в непогрешимости своего уютненького язычка.
Вообще, по определению предынкремент выполняется до вычисления выражения. Если считать ++i + ++i одним выражением, то мы должны выполнить два инкремента, потом вычислить выражение и получить 14. Если считать каждый ++i выражением, а сумму — выражением, принимающим результаты других выражений как аргументы, то, с точки зрения логики и математики, ничего таки не изменится, а вот компилятор, вычислив одно слагаемое (имеет право — самостоятельное выражение ведь!), упустит тот момент, что при вычислении второго такого же самостийного слагаемого первое тоже, сцуко, подло изменилось. Указать синтаксисом, где начинается и заканчивается выражение, невозможно — скобки задают приоритет внутри выражения. «Выражение» — другая инстанция. Например, в А=(В=С+14) есть выражение С+14, результат которого присваивается В, причем присваивание — тоже является выражением, и его результат присваивается А. И никакие скобки не могут побудить ЭТО стать одним выражением. Разъединить выражение, напротив, можно: (a=++i) + (b=++i), что напрямую задает вычисление a, вычисление b и только потом вычисление суммы a+b. Это, теоретически, должно детерминировать значение a+b как 13 (если компилятор настолько строго выдерживает формальные определения языка), но зато содержит UB относительно значений a и b, поскольку оптимизировать порядок вычисления a и b компилятору никто не запрещает (см. точки следования). Очевидно, что значение (a=++i) / (b=++i) по этой причине вообще не детерминировано.
Оно на башорге
Исходная цитата
KoloDen
Привет, я общительный пацан, люблю поболтать, особенно с классными девченками. Но, чтобы поговорить со мной, ответьте на простую задачку анти-спам бота. Вот она: int i = 5; i = ++i + ++i; Вопрос: Чему равно i?
Stefmania 14
KoloDen Гы. Признайся, ты не девченка, а 40-летний одмин, да?
Последствия
11 мая 2007 года случилось страшное — была заапрувлена вышеприведенная цитата. С тех пор разнокалиберные программисты потеряли покой и сон. Дело в том, что в зависимости от используемого языка программирования данное выражение может давать и 13, и 12, и еще больше 9000 вариантов ответа.
Реализации
C и С++
В этих языках может получиться и 13, и 14, и вообще чёрт-те-что. Разные компиляторы С++ выдают 13 и 14. Это пример неопределённого поведения. Неопределённое поведение — самый типичный для этих языков способ выстрелить себе в ногу.
Более того, один и тот же компилятор может выдавать разные значения в зависимости от опций оптимизатора. Некоторые об этой фигне еще и предупреждают, например gcc -Wall выдает warning: operation on ‘i’ may be undefined.
Даже в одной и той же программе может получаться разный результат:
int i=5,j=5;
i=++i+ ++i;
printf("i=%i j=%i", i, ++j + ++j); //Вывод: i=14 j=13
Вот описанный майндфак в квадрате на плюсах:
int i = 0; int z = ++i + ++i + ++i + ++i;
Алсо, в C++17 сабж уже не даёт UB (описали-таки поведение в стандарте), так что правильный ответ — 14 (оба раза). Можно проверить на вышеприведённом коде в любом онлайн компиляторе.
Java
В Java получается 13 (первый пре-инкремент увеличивает i на 1 и возвращает 6, второй пре-инкремент увеличивает i на 1 и возвращает 7), и данное поведение жёстко определено, так как в спецификации языка чётко описан порядок вычисления (не путать с приоритетом операторов).
C#
Как истинный клон Джавы, даёт нам результат с числом 13.
Objective-C
Компилятор clang, который является де-факто стандартом (а других просто нет), предупреждает о Multiple unsequenced modifications to 'i', но исправно возвращает 13.
awk
Awk, как и Жаба, выдаёт 13:
awk 'BEGIN { i=5;j=5;i=++i+ ++i; print i, ++j+ ++j}'
13 13
ActionScript 3.0
var test:int = 5;
trace(++test + ++test);//13
test = 5;
trace(test++ + test++);//11
ActionScript 2.0
i=5; trace(++i + ++i); //13
i=5; trace(i++ + i++); //11
Perl
Perl выдает 14:
my $i = 5;
$i = ++$i + ++$i;
print $i;
Pawn
main()
{
new i = 5, j = 5;
i = ++i + ++i;
printf "%d %d", i, ++j + ++j;
}
Вывод: 13 13
PHP
PHP выдаёт 13:
$i = 5;
$i = ++$i + ++$i;
echo $i;
JavaScript
JavaScript в V8 выдаёт 13:
i = 5
i = ++i + ++i
document.write(i)
Bash
GNU bash 4.1.5 тоже выдаёт 13:
~$ i=5; echo $((++i + ++i))
13
Forth
Никаких UB. Слова выполняются строго последовательно. Что написано, то и будет получено.
5 1+ dup + .
Выдает 12.
5 1+ dup 1+ + .
Выдает 13.
Fortran 90
Ожидаемое 13 ((а эти товарисчи и инкремент написать нормально не могут))
program mindfuck
integer i
i = 5
i = INC (i) + INC (i)
print *,i
contains
integer function INC (i)
integer,intent(inout)::i
i=i+1
INC = i
end function INC
end program mindfuck
Common Lisp
CLISP 2.49 возвращает 13, поведение определено в спецификации.
[1]> (defvar i 5)
I
[2]> (+ (incf i) (incf i))
13
Python
Python выдаёт 10, в нем нет инкремента в таком виде:
i = 5
i = ++i + ++i
print i
Но при этом, если реализовать инкремент самим, будет 13:
class Foo:
def __init__(self, num):
self.num = num
def inc(self):
self.num += 1
return self.num
i = Foo(5)
print(i.inc() + i.inc())
Это потому, что интерпретатор вычисляет все по порядку.
Но если чуток подумать и довести все до логического ума, то все таки 14
class Foo:
def __init__(self, num):
self.num = num
def inc(self):
self.num += 1
return self
def __add__(self, right):
return Foo(self.num + right.num)
def __repr__(self):
return repr(self.num)
i = Foo(5)
print(i.inc() + i.inc())
VB
Visual Basic выдаёт 10 тоже:
i = 5
i = ++i + ++i
log.WriteLine("i = " & i)
Powershell
Powershell выдаёт 13:
$i=5
$i=++$i + ++$i
Write-Host $i
Rexx
Rexx выдаёт 10
i=5
i=++i + ++i
SAY i
Delphi
В Delphi нельзя присвоить значение результата инкремента, потому что инкремент — не функция, но процедура, посему сия операция будет несовместима по типу, и компилятор откажется собирать программу, ругнувшись [Error] Incompatible types: 'Integer' and 'procedure, untyped pointer or untyped parameter' Однако, если реализовать инкремент как функцию, чтоб она работала именно так, как надо (изменяя переменную, от которой запускается), выдаст 13
function MyInc(var i: integer): integer;
begin
inc(i);
Result:=i;
end;
begin
i:=5
i:=MyInc(i){Теперь i=6}+MyInc(i){А теперь i=7}; // i:= 6 + 7;
Writeln(i); Readln // i= 13;
end;
Pascal
В Паскале нет оператора ++, но есть два оператора + (как в математике): бинарный (например, 2+2) и унарный (например, +2). Итого получается: +i — это +5, то есть то же 5; ++i — это +(+5) — ну, если мы очень настаиваем, чтобы у 5 ни в коем случае не меняли знак, почему бы и нет? В итоге, команда математически превращается в +(+5)+(+(+5))=10. Если реализовать инкремент как функцию, выдается 13.
program ippProblem;
var
i:Cardinal;
function inc(var x:Cardinal):Cardinal;
begin
x := x + 1;
inc := x;
end;
begin
i := 5;
writeln(inc(i) + inc(i));
end.
Развитие идеи
Утверждается, что есть люди, которым приведённого выражения недостаточно для полной потери церебральной девственности. Такие люди могут захотеть отступить от классики и изучить вопрос последствий выражения