// 1. В поиске я нашёл, что это UB.
cout << a++ << a++;
// 2. А так?
cout.print(a++).print(a++);
// 3. А вот так?
cout.operator <<(a++).operator <<(a++);
Пусть cout — класс, имеющий оператор << и функцию print, которые реализованы абсолютно одинаково, и возвращают ссылку на this.
Кё>// 1. В поиске я нашёл, что это UB.
Кё>cout << a++ << a++;
Кё>// 2. А так?
Кё>cout.print(a++).print(a++);
Кё>// 3. А вот так?
Кё>cout.operator <<(a++).operator <<(a++); // эквивалентно первому случаю
Кё>
Кё>Пусть cout — класс, имеющий оператор << и функцию print, которые реализованы абсолютно одинаково, и возвращают ссылку на this.
Дело не в функции print или операторе <<, а в том, что в выражении между 2-мя инкрементами переменной a нет точки следования. Компилятор вправе вычислить оба инкремента до вызова первого print.
Кё>// 1. В поиске я нашёл, что это UB.
Кё>cout << a++ << a++;
Кё>
Кё>// 2. А так? Кё>
Кё>cout.print(a++).print(a++);
Кё>
так нельзя т.к. print это функция. мы можем следать только так.
cout.print(a++);
cout.print(a++);
Кё>// 3. А вот так? Кё>
Кё>cout.operator <<(a++).operator <<(a++);
Кё>
во первых "operator <<" не пишется а записывается просто <<
соответственно имеем
cout << a++ << a++; (если конечно убрать "." которые являются в данном случае синтаксические ошибки).
Кё>Пусть cout — класс, имеющий оператор << и функцию print, которые реализованы абсолютно одинаково, и возвращают ссылку на this.
Кё>// 1. В поиске я нашёл, что это UB.
Кё>cout << a++ << a++;
Кё>// 2. А так?
Кё>cout.print(a++).print(a++);
Кё>// 3. А вот так?
Кё>cout.operator <<(a++).operator <<(a++);
Кё>
Кё>Пусть cout — класс, имеющий оператор << и функцию print, которые реализованы абсолютно одинаково, и возвращают ссылку на this.
Не имеет значения. Оба автоинкремента независимы друг от друга, поэтому между ними нет точки следования.
Если бы здесь operator++ был функцией, перегруженной для пользовательского типа, то точка следования появилась бы — вход в оператор++, выход из оператора, вход в следующий оператор++ — но порядок был бы неспецифицирован.
Кстати, operator<< — это не член ostream, а внешняя функция.
operator<<( // второйoperator<<( // первый
cout,
a++
),
a++
);
Перекуём баги на фичи!
Re[2]: Теоретический вопрос про cout << a++ << a++;
Здравствуйте, Глеб Алексеев, Вы писали:
Кё>>Пусть cout — класс, имеющий оператор << и функцию print, которые реализованы абсолютно одинаково, и возвращают ссылку на this. ГА>Дело не в функции print или операторе <<, а в том, что в выражении между 2-мя инкрементами переменной a нет точки следования. Компилятор вправе вычислить оба инкремента до вызова первого print.
Что-то я плохо понимаю. Допустим, поведение четко не определено; но какие могут быть вообще варианты?
1. Вызвать оператор ++ 10 раз подряд, а все вызовы произвести по очереди с первоначальным значением a?
3. Модифицировать 10 раз, запомнить все промежуточные результаты, и все вызовы произвести с этими результатами, подставленными в случайном порядке? Т.е. если int a = 0, то может быть cout.print(2).print(1).print(0).print(3); и a равное 4 ?
...ещё?
Re[2]: Теоретический вопрос про cout << a++ << a++;
Здравствуйте, Кодт, Вы писали:
К>Кстати, operator<< — это не член ostream, а внешняя функция. К>operator<<( // второй К> operator<<( // первый К> cout, К> a++ К> ), К> a++ К>);
Хм... дошло
Меня смутил оператор точка и его ассоциативность. Тогда как на самом деле получается
Здравствуйте, Кодёнок, Вы писали:
Кё>Что-то я плохо понимаю. Допустим, поведение четко не определено; но какие могут быть вообще варианты?
По идее, правильнее не писать программы, которые от этого будут зависеть.
Из интереса можно поэкпериментировать.
Подопытный кролик:
Здравствуйте, Глеб Алексеев, Вы писали:
ГА>Здравствуйте, Кодт, Вы писали:
К>>Кстати, operator<< — это не член ostream, а внешняя функция. ГА>Для встроенных типов это именно член ostream.
Не для всех. Контрпример — char
Re[4]: Теоретический вопрос про cout << a++ << a++;
ГА>VC 7.1 Standard edition, Debug:
ГА>preincrement: 4444
ГА>postincrement: 3210
ГА>VC 7.1 Standard edition, Release:
ГА>preincrement: 4444
ГА>postincrement: 0000
ГА>gcc 3.4.4 (Cygwin), с оптимизацией и без
ГА>preincrement: 1234
ГА>postincrement: 0123
ГА>
GCC прав, VC — баг, однозначно!
С чего это вдруг "Компилятор вправе вычислить оба инкремента до вызова первого print"?
На основании чего он вправе это делать? Есть таблица "Operator Associativity and Precedence" в соотв. с которой VC — бажит.
Выражение
cout << "preincrement: " << ++a << ++a << ++a << ++a << endl;
// это
((((((cout << "preincrement: ") << ++a) << ++a) << ++a) << ++a) << endl);
// левый операнд в операторе << вычисляется раньше правого, значит это:
cout << "preincrement: ";
cout << ++a;
cout << ++a;
cout << ++a;
cout << ++a;
cout << endl;
// или
cout << "preincrement: ";
cout << a; a = a + 1;
cout << a; a = a + 1;
cout << a; a = a + 1;
cout << a; a = a + 1; // прошу не цепляться за a = a + 1, это не "буквально"
cout << endl;
Здравствуйте, Кодёнок, Вы писали:
Кё>// 1. В поиске я нашёл, что это UB. Кё>cout << a++ << a++;
Это не UB по стандарту, а код который может вызвать UB на глючных компиляторах.
Кё>// 1. В поиске я нашёл, что это UB.
Кё>cout << a++ << a++;
Кё>// 2. А так?
Кё>cout.print(a++).print(a++);
Кё>// 3. А вот так?
Кё>cout.operator <<(a++).operator <<(a++);
Кё>
Кё>Пусть cout — класс, имеющий оператор << и функцию print, которые реализованы абсолютно одинаково, и возвращают ссылку на this.
Здесь UB надо понимать unspecified.
Ассоциативность говорит о том кто к кому липнет, то есть
о расстановке скобок:
(cout << (a++)) << (a++);
Так как оператор << переопределен, то для него как и для функции
есть точки следования. Значит это не undefined. С другой стороны
последовательность вычисления операндов в правом операторе не
специфицирована. Если сначала будет вычеслен a++, то для
начального значения a = 0, получим
10
Если сначала вычисляется левый оператор <<, получим
01
Верь мне,
Лазар (MCSD)
Re[2]: Теоретический вопрос про cout << a++ << a++;
От:
Аноним
Дата:
10.10.05 14:14
Оценка:
ЛБ>Так как оператор << переопределен, то для него как и для функции ЛБ>есть точки следования.
Точка следования есть после вычисления аргументов. Но никто не гарантирует, что если есть несколько вызовов функции в одном выражении, то сначала вычислятся аргументы только одной функции и идет точка следования, и только потом вычилсяются аргументы другой функции.
Re[5]: Теоретический вопрос про cout << a++ << a++;
Здравствуйте, Chez, Вы писали:
C>GCC прав, VC — баг, однозначно! C>С чего это вдруг "Компилятор вправе вычислить оба инкремента до вызова первого print"? C>На основании чего он вправе это делать? Есть таблица "Operator Associativity and Precedence" в соотв. с которой VC — бажит.
Это глубочайшее заблуждение — полагать, что ассоциативность и предшествование здесь играют роль.
Данная таблица описывает свойства синтаксиса и определяет правила расстановки скобок. Не более того.
VC не бажит в этой области.
Вот рассмотрим пример без UB. (a = b = x / y / z / t)
Согласно правилам ассоциативности, скобки будут расстановлены так:
А теперь обоснуй, в каком порядке должны вычисляться подвыражения x,y,z,t для того, чтобы посчитать (x/y), (x/y)/z, ((x/y)/z)/t) и выполнить присваивание?
Очевидно, в произвольном!
Именно это и значит, что если эти подвыражения содержат внутри себя точки следования (например, это вызовы функций), то их побочные эффекты следуют в произвольном порядке и дают unspecified behavior; а если точек следования нет (например, это автоинкремент), то их взаимное влияние даёт undefined behavior.
Почему же так жёстко? Почему нельзя заявить, что любая модификация переменной (будь то присваивание или автоинкремент) создаёт точку следования (и тем самым, получить всего лишь unspecified behavior — что, в общем-то, тоже не подарок)? Ответ: из соображений оптимизации.
Об этом уже много сказано, рекомендую поискать по форуму.
Перекуём баги на фичи!
Re[3]: Теоретический вопрос про cout << a++ << a++;
Здравствуйте, Аноним, Вы писали:
ЛБ>>Так как оператор << переопределен, то для него как и для функции ЛБ>>есть точки следования.
А>Точка следования есть после вычисления аргументов. Но никто не гарантирует, что если есть несколько вызовов функции в одном выражении, то сначала вычислятся аргументы только одной функции и идет точка следования, и только потом вычилсяются аргументы другой функции.
5.2.2/1
... A function call is a postfix expression followed by parentheses
containing a possibly empty, comma separated list of expressions which
constitute the arguments to the function. ...
5.2.2/5
The order of evaluation of arguments is unspecified.
...
The order of evaluation of the postfix expression and
the argument expression list is unspecified.
Так что вызов функции это почти как бинарный оператор.
Последовательность вычисления двух подвыражений не специфицируется,
но предполагать, что начав вычислять одно выражение, компилятор
вдруг остановится и повычисляет "немоножечко" другого выражения,
это слишком.
Лазар
Re[4]: Теоретический вопрос про cout << a++ << a++;
Здравствуйте, Лазар Бешкенадзе, Вы писали:
ЛБ>но предполагать, что начав вычислять одно выражение, компилятор ЛБ>вдруг остановится и повычисляет "немоножечко" другого выражения, ЛБ>это слишком.
Ну мало ли какие процессоры могут быть созданы Допустим несколько конвееров, и в каждом можно модифицировать ячейки памяти. А тип данных, который инкрементится, 64..1024-битный и за одну атомарную операцию точно не выполняется. А существовать такие процессоры могут исключительно в рассчете, что писать будут для умных компиляторов, которые не сгенерируют кода с неопределенным поведением для программы без оного. В этом случае оптимизатор имеет право расставить код вычисления `a++` так, что он будет выполняться в двух конвеерах.
Re[5]: Теоретический вопрос про cout << a++ << a++;
Здравствуйте, Кодёнок, Вы писали:
Кё>Здравствуйте, Лазар Бешкенадзе, Вы писали:
ЛБ>>но предполагать, что начав вычислять одно выражение, компилятор ЛБ>>вдруг остановится и повычисляет "немоножечко" другого выражения, ЛБ>>это слишком.
Кё>Ну мало ли какие процессоры могут быть созданы Допустим несколько конвееров, и в каждом можно модифицировать ячейки памяти. А тип данных, который инкрементится, 64..1024-битный и за одну атомарную операцию точно не выполняется. А существовать такие процессоры могут исключительно в рассчете, что писать будут для умных компиляторов, которые не сгенерируют кода с неопределенным поведением для программы без оного. В этом случае оптимизатор имеет право расставить код вычисления `a++` так, что он будет выполняться в двух конвеерах.
Был неправ. Все-таки на работе надо делать работу.
Sequence point стоит после вычисления _всех_ аргументов, но до
выполнения тела функции.
Как там было?
(cout << (a++)) << (a++)
Например имеем вычисленный но без завершенных побочных эффектов
правый a++. Начинаем вычислять операнды для левого << и на этом
самом sequence point для левого << получаем незавершенные side
effects от обоих инкрементов.
Еще раз пардон.
Лазар
Re[6]: Теоретический вопрос про cout << a++ << a++;
Здравствуйте, Кодт, Вы писали:
К>Это глубочайшее заблуждение — полагать, что ассоциативность и предшествование здесь играют роль. К>Данная таблица описывает свойства синтаксиса и определяет правила расстановки скобок. Не более того.
Оч. странно. Ведь когда внутри скобки есть 2 операнда, всегда можно сказать, какой будет вычислен первым, а какой — вторым?!! К>VC не бажит в этой области.
К>Вот рассмотрим пример без UB. (a = b = x / y / z / t) К>Согласно правилам ассоциативности, скобки будут расстановлены так: К> kromsated
К>А теперь обоснуй, в каком порядке должны вычисляться подвыражения x,y,z,t для того, чтобы посчитать (x/y), (x/y)/z, ((x/y)/z)/t) и выполнить присваивание? К>Очевидно, в произвольном!
Для меня это абсолютно не очевидно!
Вычисление подвыражений должно происходить как обычно — в направлении ассоциативности, в порядке приоритета операций
0) a = (b = (((x / y) / z) / t))
0.0) (b = (((x / y) / z) / t))
0.0.0) (((x / y) / z) / t)
0.0.0.0) ((x / y) / z)
0.0.0.0.0) (x / y) 0.0.0.0.0.0) x (1) 0.0.0.0.0.1) y (2) 0.0.0.0.1) z (3) 0.0.0.1) t (4) 0.0.1) b (5) 0.1) a (6)
Для меня сложно понять, как умные люди из комитета могли в этом месте придумать UB!
Если здесь действительно UB, то для меня это неприятная новость, и камень в огород плюсов.
К>Почему же так жёстко? Почему нельзя заявить, что любая модификация переменной (будь то присваивание или автоинкремент) создаёт точку следования (и тем самым, получить всего лишь unspecified behavior — что, в общем-то, тоже не подарок)? Ответ: из соображений оптимизации.
Далеко бы я послал эти соображения, если б меня спросили... К>Об этом уже много сказано, рекомендую поискать по форуму.
Ок. Поищу. Спасибо.
Здравствуйте, Chez, Вы писали:
C>Здравствуйте, Кодт, Вы писали:
К>>Это глубочайшее заблуждение — полагать, что ассоциативность и предшествование здесь играют роль. К>>Данная таблица описывает свойства синтаксиса и определяет правила расстановки скобок. Не более того.
C>Оч. странно. Ведь когда внутри скобки есть 2 операнда, всегда можно сказать, какой будет вычислен первым, а какой — вторым?!!
нет. предствь, что у тебя 12 процессоров на машине установлено.
К>>Вот рассмотрим пример без UB. (a = b = x / y / z / t) К>>Согласно правилам ассоциативности, скобки будут расстановлены так: К>> kromsated
К>>А теперь обоснуй, в каком порядке должны вычисляться подвыражения x,y,z,t для того, чтобы посчитать (x/y), (x/y)/z, ((x/y)/z)/t) и выполнить присваивание? К>>Очевидно, в произвольном! C>Для меня это абсолютно не очевидно! C>Вычисление подвыражений должно происходить как обычно — в направлении ассоциативности, в порядке приоритета операций C>Для меня сложно понять, как умные люди из комитета могли в этом месте придумать UB! C>Если здесь действительно UB, то для меня это неприятная новость, и камень в огород плюсов.
Они не придумали UB.
Они предоставили возможности для оптимизации.
Например, без этой возможности нельзя было бы вычисление операндов произвести параллельно на разных процессорах.
А ты просто должен знать, в каких случаях может возникнуть UB.
И если тебе надо нечто вычислить обязательно в определенном порядке, без возможности распараллеливания, используй синтаксис с явными точками следования (operator,).
К>>Почему же так жёстко? Почему нельзя заявить, что любая модификация переменной (будь то присваивание или автоинкремент) создаёт точку следования (и тем самым, получить всего лишь unspecified behavior — что, в общем-то, тоже не подарок)? Ответ: из соображений оптимизации. C>Далеко бы я послал эти соображения, если б меня спросили...
Здравствуйте, Chez, Вы писали:
К>>Это глубочайшее заблуждение — полагать, что ассоциативность и предшествование здесь играют роль. К>>Данная таблица описывает свойства синтаксиса и определяет правила расстановки скобок. Не более того. C>Оч. странно. Ведь когда внутри скобки есть 2 операнда, всегда можно сказать, какой будет вычислен первым, а какой — вторым?!!
Кому можно сказать?
Ну хорошо, давай посмотрим на такой пример
printf("%d %d %d", x(), y(), z());
Внутри скобки 4 операнда — одна константа и три вычислимых.
В соответствии с конвенцией вызова cdecl или stdcall (здесь cdecl) операнды должны располагаться на стеке в порядке
(вершина стека) (первый) (второй) (третий) (четвёртый) .....
строка x y z
Или, отвлекаясь от "лево-правого" вида, чтобы он не вгонял нас в рамки,
(вершина)
(первый) строка
(второй) x
(третий) y
(четвёртый) z
.....
Как удобнее их запихивать туда?
— вычислить x и запомнить, вычислить y и запомнить, вычислить z и положить в стек, вспомнить y и положить, вспомнить x и положить
— зарезервировать место для всех сразу, вычислить-и-положить в произвольном порядке
— вычислить z и положить, вычислить y и положить, вычислить x и положить
Для компилятора без оптимизации — наиболее эффективен последний способ.
Для человека кажется естественным первый.
С оптимизацией может быть и второй. Например,
cos, sin — это встроенные (intrinsic) функции, компилятор заменит их на команды FPU. Очевидно, что результат вычисления можно сразу положить в две ячейки стека, не отвлекаясь на вычисление sqrt(x*y).
Кстати говоря, конвенция pascal располагает аргументы в стеке "наоборот", так, чтобы естественный порядок вычислений совпал с порядком чтения.
К>>Вот рассмотрим пример без UB. (a = b = x / y / z / t) К>>Согласно правилам ассоциативности, скобки будут расстановлены так: К>> kromsated К>>А теперь обоснуй, в каком порядке должны вычисляться подвыражения x,y,z,t для того, чтобы посчитать (x/y), (x/y)/z, ((x/y)/z)/t) и выполнить присваивание? К>>Очевидно, в произвольном! C>Для меня это абсолютно не очевидно! C>Вычисление подвыражений должно происходить как обычно — в направлении ассоциативности, в порядке приоритета операций C>0) a = (b = (((x / y) / z) / t)) C>0.0) (b = (((x / y) / z) / t)) C>0.0.0) (((x / y) / z) / t) C>0.0.0.0) ((x / y) / z) C>0.0.0.0.0) (x / y) C>0.0.0.0.0.0) x (1) C>0.0.0.0.0.1) y (2) C>0.0.0.0.1) z (3) C>0.0.0.1) t (4) C>0.0.1) b (5) C>0.1) a (6) C>Для меня сложно понять, как умные люди из комитета могли в этом месте придумать UB! C>Если здесь действительно UB, то для меня это неприятная новость, и камень в огород плюсов.
Здесь не undefined, а unspecified. Если тебе пофиг, в каком порядке следуют побочные эффекты, то проблемы нет. Если не пофиг — проблемы будут.
А насчёт камней в огороды... Не бегай в валенках по конькобежной дорожке, так и не упадёшь.