Порядок вычислений аргументов A.F(...).G(...)
От: Vi2 Удмуртия http://www.adem.ru
Дата: 23.04.02 05:19
Оценка:
Я почему-то всегда считал, что функции выделенного ниже Фрагмента будут вызваны последовательно (что и есть на самом деле) и параметры будут передаваться в функцию по мере их вызова. Т.е. я ожидал распечатку в таком виде
f :i=11 j=1 f :i=12 j=2 f :i=13 j=3


А это не так. Я нашел, что эквивалент распечатки выделенного фрагмента будет распечатка от функции f3.
Почему так?

// MS VC 6.0
#include "stdafx.h"
#include <stdio.h>

char szDbgBuf[1024];

class A
{
public:
    const A& f( int i, int j ) const
    {
        sprintf(szDbgBuf,"f :i=%d j=%d ",i,j);OutputDebugString(szDbgBuf);
        return *this;
    }
};

int f3( int i1, int j1, int i2, int j2, int i3, int j3 )
{
    sprintf(szDbgBuf,"f3:i=%d j=%d ",i1,j1);OutputDebugString(szDbgBuf);
    sprintf(szDbgBuf,"f3:i=%d j=%d ",i2,j2);OutputDebugString(szDbgBuf);
    sprintf(szDbgBuf,"f3:i=%d j=%d ",i3,j3);OutputDebugString(szDbgBuf);
    return 0;
}

int APIENTRY WinMain(HINSTANCE h1, HINSTANCE h2, LPSTR lpCmd, int nCmd)
{
    A a;
    int z = 10;
    a.f( ++z,1 ).f( ++z,2 ).f( ++z,3 ); // Фрагмент
    OutputDebugString("\n");
    z = 10;
    f3( ++z,1, ++z,2, ++z,3 );
    OutputDebugString("\n");
    return 0;
}

Распечатка:
f :i=13 j=1 f :i=12 j=2 f :i=11 j=3 
f3:i=13 j=1 f3:i=12 j=2 f3:i=11 j=3
Vita
Выше головы не прыгнешь, ниже земли не упадешь, дальше границы не убежишь! © КВН НГУ
Re: Порядок вычислений аргументов A.F(...).G(...)
От: Андрей Тарасевич Беларусь  
Дата: 23.04.02 05:41
Оценка: 4 (1)
Здравствуйте Vi2, Вы писали:

Vi2>Я почему-то всегда считал, что функции выделенного ниже Фрагмента будут вызваны последовательно (что и есть на самом деле) и параметры будут передаваться в функцию по мере их вызова. Т.е. я ожидал распечатку в таком виде

Vi2>
Vi2>f :i=11 j=1 f :i=12 j=2 f :i=13 j=3 
Vi2>


Vi2>А это не так. Я нашел, что эквивалент распечатки выделенного фрагмента будет распечатка от функции f3.

Vi2>Почему так?

Фнукции действительно вызываются последовательно и параметры тоже передаются в ыункции по мере их вызова. Т.е. так, как ты и ожидал. А вот в какой момент вычисляются эти передаваемые пареметры — не определено. Отсюда и неожиданные результаты. Фрагменты кода

a.f( ++z,1 ).f( ++z,2 ).f( ++z,3 );


и

f3( ++z,1, ++z,2, ++z,3 );


содержат модификацию одного и того скалярного объекта 'z' несколько раз в рамках одного и того же выражения. Такой код порождает неопределенное поведение. Поэтому предсказать его результат никак нельзя.
Best regards,
Андрей Тарасевич
Re[2]: Параметр this ?
От: Vi2 Удмуртия http://www.adem.ru
Дата: 23.04.02 05:51
Оценка:
Здравствуйте Андрей Тарасевич, Вы писали:

АТ>Фнукции действительно вызываются последовательно и параметры тоже передаются в ыункции по мере их вызова. Т.е. так, как ты и ожидал. А вот в какой момент вычисляются эти передаваемые пареметры — не определено. Отсюда и неожиданные результаты. Фрагменты кода

АТ>содержат модификацию одного и того скалярного объекта 'z' несколько раз в рамках одного и того же выражения. Такой код порождает неопределенное поведение. Поэтому предсказать его результат никак нельзя.

Может это как-то коррелирует с тем, что неявно существует ЕЩЕ один параметр this, который должен быть вычислен? Т.е. если перевести С++ на С (примерно так), то будет, наверное, понятно поведение компилятора.
    a.f1( ++z,1 ).а1( ++z,2 ).f1( ++z,3 );
    // =>
    f1C( f1C( f1C( &a, ++z,1 ), ++z,2 ), ++z,3 )
Vita
Выше головы не прыгнешь, ниже земли не упадешь, дальше границы не убежишь! © КВН НГУ
Re[3]: Параметр this ?
От: Андрей Тарасевич Беларусь  
Дата: 23.04.02 05:57
Оценка:
Здравствуйте Vi2, Вы писали:

Vi2>Может это как-то коррелирует с тем, что неявно существует ЕЩЕ один параметр this, который должен быть вычислен? Т.е. если перевести С++ на С (примерно так), то будет, наверное, понятно поведение компилятора.


Я бы не советовал тебе в этой ситуации пытаться объяснить поведение компилятора. Множественная модификация скалярного объекта в рамках одного выражения — это undefined behavior. Пытаться искать логику в undefined behavior — совершенно бессмысленное занятие. Поведение компилятора тут совершенно непредсказуемо. Один и тот же компилятор может вести себя по-разному в двух соседних участках кода.

P.S. Не думаю, что 'this' тут на что-то влияет.
Best regards,
Андрей Тарасевич
Re[4]: Параметр this ?
От: Кодт Россия  
Дата: 23.04.02 06:58
Оценка: 4 (1)
Здравствуйте ALL

Я могу объяснить.
Итак, допустим, что используется конвенция stdcall

Перепишем выражение...

a1.f1(z1(), 1).f2(z2(), 2).f3(z3(), 3)
-- для наглядности переименовал ++z и f

a2 = f1(z1(), 1)
a3 = f2(z2(), 2)
a4 = f3(z3(), 3)

а теперь - будем хранить временные значения (а2, а3) на стеке

команда                       стек
===============+==================
push 3         |                 3
push & call z3 |              z3 3
pubh 2         |            2 z3 3
push & call z2 |         z2 2 z3 3
push 1         |       1 z2 2 z3 3
push & call z1 |    z1 1 z2 2 z3 3
push a1        | a1 z1 1 z2 2 z3 3 -- наконец-то! можем положить a
call f1        |      a2 z2 2 z3 3
call f2        |           a3 z3 3
call f3        |                a4


Вот так вот, любители cin/cout!

Для паскаля, кстати, (где другая конвенция), все не так ужасно
push a1        | a1
push & call z1 | z1 a1
push 1         | 1 z1 a1
call f1        | a2
push & cll z2  | z2 a2
push 2         | 2 z2 a2
call f2        | a3

etc...


Если бы компилятор думал своей головой и создал временную локальную переменную, то можно было бы и stdcall соблюсти:
mov temp, a1   |         | a1
push & call z1 |      z1 | a1
push 1         |    1 z1 | a1
push temp      | a1 1 z1 | a1
call f1        |      a2 | a1
pop temp       |         | a2

etc...


Но для дебага жизнь кажется более простой, у него есть дерево выражения, которое надо перевести в операции push и call
Перекуём баги на фичи!
Re[4]: Параметр this ?
От: Кодт Россия  
Дата: 23.04.02 07:01
Оценка:
Здравствуйте Андрей Тарасевич, Вы писали:

АТ>P.S. Не думаю, что 'this' тут на что-то влияет.


В общем, именно this и повлиял.
Ну, и конвенция stdcall тоже.
Перекуём баги на фичи!
Re[5]: Параметр this ?
От: Андрей Тарасевич Беларусь  
Дата: 23.04.02 15:51
Оценка:
Здравствуйте Кодт, Вы писали:

АТ>>P.S. Не думаю, что 'this' тут на что-то влияет.


К>В общем, именно this и повлиял.

К>Ну, и конвенция stdcall тоже.

Никак не могу въехать, где ты тут видишь влияние 'this' и конвенции 'stdcall'? Приведенный пример является классическим примером, демонстрирующим undefined behavior возникающий при попытке модифицировать одно и то же скалярное значение несколько раз между парой sequence points. Предсказать поведение этого кода невозможно ни с 'this' и 'stdcall', ни без них. Причем это не какой-нибудь теоретический undefined behavior, это самый настоящий практический undefined behavior, который всегда себя проявляет тем или иным образом. Этот (и аналогичные) пример может порождать разные результаты при компиляции отладочной или релизной версии, при включении/выключении оптимизаций и т.п.

Твой вариант, в котором ты заменил '++z' на вызовы функций 'z1', 'z2' и 'z3', не является эквивалентом исходного варианта. Здесь важно именно то, что изменение 'z' не заключено в функцию. Имено в этом случае мы получим множественную модификацию между парой sequence points.

Можно попытаться приллюстрировать проблему (хотя иллюстрировать undefined behavior — занятие неблагодарное). В ответ на

a.f( ++z,1 ).f( ++z,2 ).f( ++z,3 );


Компилятор имеет право сгенерировать и такой код:

int t1 = ++z;
int t2 = ++z;
int t3 = ++z;
a2 = a1.f1(t1, 1);
a3 = a1.f2(t2, 2);
a3.f3(t3, 3);


А также он имеет право сгенерировать такой код:

int t3 = ++z;
int t2 = ++z;
int t1 = ++z;
a2 = a1.f1(t1, 1);
a3 = a1.f2(t2, 2);
a4 = a3.f3(t3, 3);


Или такой:

int t1 = ++z;
a2 = a1.f1(t1, 1);
int t2 = ++z;
a3 = a1.f2(t2, 2);
int t3 = ++z;
a3.f3(t3, 3);


Во всех этих случаях результат будет разным. Это и есть ответ на исходный вопрос. Никакой 'this' или 'stdcall' тут соврешенно ни при чем.
Best regards,
Андрей Тарасевич
Re[6]: Параметр this ?
От: Кодт Россия  
Дата: 23.04.02 16:20
Оценка: 10 (2)
Здравствуйте Андрей Тарасевич

Все совершенно верно про неопределенное поведение.
Любая функция с побочным эффектом (а оператор автоинкремента таковым является) способна дать такой же результат.

Тут я спорить не буду.

(И вообще такие фокусы делать нехорошо, дорогие любители cin/cout).

Просто логика работы компилятора была, что называется, налицо.
И именно об этом я и написал.

Строится дерево выражения
a.f(z1, 1).f(z2, 2).f(z3, 3)

A::f(A::f(A::f(a, z1, 1), z2, 2), z3, 3)

- это была прямая польская запись

обратная польская запись:

stdcall

3 z3 2 z2 1 z1 a A::f A::f A::f

pascal

a z1 1 A::f z2 2 A::f z3 3 A::f


В дебаге компилятор транслирует выражение наиболее халявным способом.
А это — вывалить все на стек и не использовать регистры или временные переменные. Именно здесь конвенция и играет роль.

С оптимизацией он (может быть) догадается завести временную переменную, а то и вообще обойдется регистрами.

Кстати, только запятая и логические операторы ( && и || ) имеют жесткий порядок вычисления операндов (слева направо).

А если их перегрузить — будет ли компилятор придерживаться этого правила?

z = 10;
(f(++z, 1), true) && (f(++z, 2), true);

// получится f(11,1), f(12,2) ?

class A
{
public:
  const A& operator && (int z) const { g(z); return this; }
};

A a;
z = 10;
a && (++z) && (++z);

// получится теперь g(11), g(12) ?

Перекуём баги на фичи!
undefined и unspecified: разница между операциями и функциям
От: Андрей Тарасевич Беларусь  
Дата: 23.04.02 17:36
Оценка: 104 (18)
#Имя: FAQ.cpp.undefined
К>Все совершенно верно про неопределенное поведение.
К>Любая функция с побочным эффектом (а оператор автоинкремента таковым является) способна дать такой же результат.

Не совсем так. Важным является тот факт, что несколько модификаций одного скалярного объекта оказались зажатыми между двумя соседними точками следования. Именно это приводит к неопределенному поведению.

Если заключить модификацию переменной 'z' в отдельную функцию

int incz(inr& z) { return ++z; }


и переписать вызов вот так:

a.f( incz(z1) , 1).f( incz(z1) , 2).f( incz(z1) , 3);


то теперь мы получим, что каждое изменение переменной 'z' "зажато" между парой точек следования — каждая функция имеет точку следования на входе и на выходе (в данном случае — функция 'incz'). Теперь у нас уже нет ситуации, когда один и тот же скалярный объект модифицируется несколько раз между парой соседних точек следования. Поэтому теперь у нас нет неопределенного поведения.

Но тем не менее, хотя неопределенного поведения у нас больше нет, предсказать результат выполнения этого кода все равно нельзя. Почему? Потому что теперь в игру вступают два других правила С++:

1) Порядок вычисления подвыражений в рамках одного выражения не определен.
2) Порядок вычичления параметров функции не определен.

(А может быть правило 2 — это просто частный случай правила 1). Не надо пугаться присутствия в этих правилах слов "не определен". К неопределеному поведению (undefined behavior) они никакого отношения не имеют.

Очень важно не путать ситуацию, когда выражение порождает неопределенное поведение, и когда выражение не порождает неопределенного поведения, но его результат зависит от порядка вычисления. Это — разные ситуации. Данный пример не очень хорошо иллюстрирует это разницу.

Чтобы получше ее проиллюстрировать, я приведу вот такой пример:

int a[] = { 1, 2, 3 };
int* p = a;
int s = *p++ + *p++ + *p++; // (1)


Чему будет равно 's'?

Часто можно услышать такие рассуждения по этому поводу: "Да, я знаю, что порядок вычисления подвыражений выражения (1) не определен. Но в данном случае, как ни верти, все равно получается одно и то же. Либо '1 + 2 + 3', либо '3 + 2 + 1', либо '2 + 1 + 3', либо еще как — всегда в сумме получается 6. Значит переменная 's' пронициализируется значением 6."

Когда же обнаруживается, что переменная 's' инициализируется не значением 6, начинаются разговоры про "глюки компилятора" и т.п. На самом деле компилятор ни в чем не виноват. Выражение (1) порождает неопределенное поведение, потому что оно содержит несколько модификаций одного скалярного объекта 'p' между двумя соседними точками следования. Неопределенное поведение — этим все сказано.

А теперь давайте модифицируем это пример вот так:

int getp(int*& p) { return *p++; }

int a[] = { 1, 2, 3 };
int* p = a;
int s = getp(p) + getp(p) + getp(p); // (2)


Теперь у меня модификация указателя делается внутри функции 'getp', т.е. каждая модификация "обрамлена" своей парой точек следования — на входе в функцию 'getp' и на выходе из нее. Теперь выражение (2) уже не порождает неопределенного поведения. Можно ли теперь сказать, чему будет равна переменная 's'. Да, можно. Она будет равна 6. Да, порядок вычисения подвыражений выражения (2) по-прежнему не определен. Но результат действительно не зависит от этого порядка! И '1 + 2 + 3', и '3 + 2 + 1', и все остальные варианты дают в результате именно 6.

Это пример очень хорошо иллюстрирует разницу между неопределенным поведением и неопределенным порядком вычисления подвыражений целого выражения.

По отношению к исходному примеру твое утверждение "Любая функция с побочным эффектом (а оператор автоинкремента таковым является) способна дать такой же результат" — неверно. Оператор инкремента, примененный к скалярному типу, функцией не является. Следствием этого является то, что модификации, производимые таким оператором инкремента, не отделены от "окружающего мира" при помощи точек следования. Этого достаточно для того, чтобы перейти тонкую границу и попасть в область неопределенного поведения. В случае функции с побочным эффектами этого бы не произошло. Мой пример это очень хорошо иллюстрирует.

К>Просто логика работы компилятора была, что называется, налицо.

К>И именно об этом я и написал.
К>Строится дерево выражения

Твои рассуждения могли бы быть перекрасной иллюстрацией логики компилятора, если бы мы имели дело со случаем, в котором поведение программы зависело бы от порядка вычисления подвыражений целого выражения. Именно это ты иллюстрируешь. Но в данном случае, еше раз повторюсь, мы имеем дело с на порядок более "запущенной" ситуацией — неопределенным поведением. Пытатсья объяснить логику компилятора здесь — занятие неблагодарное. Ее может просто не быть.
Best regards,
Андрей Тарасевич
Re[8]: Есть еще ма-а-аленький вопрос.
От: Vi2 Удмуртия http://www.adem.ru
Дата: 24.04.02 03:48
Оценка:
Андрей Тарасевич и Кодт, спасибо за содержательные ответы.
Особенно за про неопределенное поведение и за точки следования.
Но теперь вот какой вопрос по следующему коду.
    int z = 1;
    int z2 = z + ++z + ++z;
    z = 1;
    int z3 = ++z + z + ++z;
    z = 1;
    int z4 = ++z + ++z + z;
    sprintf(szDbgBuf,"%d %d %d\n",z2,z3,z4);
    OutputDebugString(szDbgBuf);
Результат: 7 7 9 (MS VC60)

Имеет ли право компилятор подставлять значение z (там, где оно является операндом, я его выделил) отличное от того, которое оно имело ДО начала вычисления выражения, стоящее за знаком "=" ?
Насколько я помню по теории алгоритмов выражение переводит одно состояние в другое, причем предыдущее состояние не меняется и определяет следующее. Поэтому, кажется, каждый из ++ определяет какие-то дополнительные состояния. Т.е. мне не совсем понятна роль этих дополнительных состояний.
Vita
Выше головы не прыгнешь, ниже земли не упадешь, дальше границы не убежишь! © КВН НГУ
Re[7]: Я не любитель cin/cout
От: Vi2 Удмуртия http://www.adem.ru
Дата: 24.04.02 03:53
Оценка:
Здравствуйте Кодт, Вы писали:

К>(И вообще такие фокусы делать нехорошо, дорогие любители cin/cout).


Я — не любитель cin/cout, это же очевидно из примера. То, что вопрос навеял пример с cin/cout, это не о чем не говорит. Тут скорее всего влияние скриптов и неприятие безоглядного увлечения перегрузкой (по крайней мере в моей кампании) с возвратом A& A::xxx. Ведь то, что обходится (или задействуется) на одном компиляторе, скорее всего работать не будет на другом или же на этом, но более поздней версии.
Vita
Выше головы не прыгнешь, ниже земли не упадешь, дальше границы не убежишь! © КВН НГУ
Re[9]: Есть еще ма-а-аленький вопрос.
От: Андрей Тарасевич Беларусь  
Дата: 24.04.02 04:27
Оценка: 23 (3)
Здравствуйте Vi2, Вы писали:

Vi2>Но теперь вот какой вопрос по следующему коду.

Vi2>
Vi2>    int z = 1;
Vi2>    int z2 = z + ++z + ++z;
Vi2>    z = 1;
Vi2>    int z3 = ++z + z + ++z;
Vi2>    z = 1;
Vi2>    int z4 = ++z + ++z + z;
Vi2>    sprintf(szDbgBuf,"%d %d %d\n",z2,z3,z4);
Vi2>    OutputDebugString(szDbgBuf);
Vi2>Результат: 7 7 9 (MS VC60)
Vi2>

Vi2>Имеет ли право компилятор подставлять значение z (там, где оно является операндом, я его выделил) отличное от того, которое оно имело ДО начала вычисления выражения, стоящее за знаком "=" ?

Да, имеет. Он имеет право сделать вообще все, что угодно. Эти выражения страдают от той же самой проблемы, которая имеет место в твоем исходном примере. Переменная 'z' скалярного типа модифицируется между парой соседних точек следования более одного раза. Это выражение порождает неопределенное поведение. Компилятор имеет правло сгенерировать код, в результате выполнения котрого программа просто рухнет.

Vi2>Насколько я помню по теории алгоритмов выражение переводит одно состояние в другое, причем предыдущее состояние не меняется и определяет следующее. Поэтому, кажется, каждый из ++ определяет какие-то дополнительные состояния. Т.е. мне не совсем понятна роль этих дополнительных состояний.


Во-первых, в С++ разделителями состояний являются не выражения, а точки следования. Во многих случаях (в большинстве случаев) расположение точек следования совпадает с границами выражений. Но может и не совпадать.

Во-вторых, между двумя точками следования нет никаких дополнительных состояний. Они запрещены стандартом языка. Компилятор не занимается проверкой этого запрещения, они запрещены именно декларативно. Т.е. стандарт языка запрещает прграммисту писать код, который порождает такие промежуточные состояния. Если программист пишет такой код, то за последствия отвечает он сам.

В стандарте языка сказано, что

a) между парой соседних точек следования переменная скаларного типа может быть модифицирована не более одного раза и
b) если такая переменная модифицируется, то читать ее старое значение разрешается только для того, чтобы вычислить ее новое значение.

Твой код порождает неопределенное поведение согласно первой половине этого правила. Вот такой код

int z = 1;
int z2 = z + ++z;


тоже порождает неопределенное поведение, согласно второй половине этого правила.

Рассуждать о каких-то промежуточных состояниях по отношению к этому коду нет никакого смысла, потому что совершенно невозможно определить, что этот код означает. Этот код просто-напросто некорректен.
Best regards,
Андрей Тарасевич
Re: Хотел спросить...
От: NightWind Россия  
Дата: 04.05.07 12:36
Оценка:
Решил не создавать новую тему...
В свете вышесказанного можно ли точно сказать чему будут равны b1 и b2

void f()
{
int a1 = 3;
int a2 = 3;
int b1 = a1++ == 3 ? a1 : 0;
int b2 = ++a2 == 4 ? a2 : 0;
}
Re[2]: Хотел спросить...
От: NightWind Россия  
Дата: 04.05.07 13:53
Оценка:
Здравствуйте, NightWind, Вы писали:


NW>Решил не создавать новую тему...

NW>В свете вышесказанного можно ли точно сказать чему будут равны b1 и b2

NW>
NW>void f()
NW>{
NW>int a1 = 3;
NW>int a2 = 3;
NW>int b1 = a1++ == 3 ? a1 : 0;
NW>int b2 = ++a2 == 4 ? a2 : 0;
NW>}
NW>


Вроде сам разобрался, поправьте, если ошибаюсь:

b1 == 4 и b2 == 4

Поскольку в первом операнде оператора ?: находится точка следования.

5.15/1
All side effects of the first expression except for destruction of
temporaries (12.2) happen before the second or third expression is evaluated.

 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.