Точки последовательности и цепочка методов

Следующее выражение часто используется для демонстрации неуказанного поведения undefined:

f() + g()

Если f() и g() имеют побочные эффекты для какого-либо общего объекта, тогда поведение undefined не указано, поскольку порядок выполнения неизвестен. f() может оцениваться перед g() или наоборот.

Теперь мне было интересно, что происходит, когда вы связываете функции-члены с объектом. Допустим, у меня есть экземпляр класса, экземпляр с именем obj, и у него есть две функции-члена, foo() и bar(), которые изменяют объект. Порядок выполнения этих функций не коммутативен. Эффект от вызова одного перед другим отличается от вызова их наоборот. Оба метода возвращают ссылку на *this, чтобы их можно было связать следующим образом:

obj.foo().bar()

Но является ли это неопределенным поведением? Я не могу найти ничего в стандарте (по общему признанию, просто просматривая), что отличает это выражение от выражения, которое я дал вверху поста. Оба вызова функций являются подвыражениями полного выражения, поэтому порядок их выполнения не определен. Но, конечно же, foo() должен сначала оцениваться, чтобы bar() знал, какой объект модифицировать.

Возможно, я упускаю что-то очевидное, но я не вижу, где создается точка следования.


person Joseph Mansfield    schedule 02.04.2011    source источник
comment
Поведение обязательно определено. Но я не мог указать вам на соответствующую часть стандарта.   -  person Alexandre C.    schedule 02.04.2011
comment
Именно такая проблема у меня. Я знаю, что, конечно, это должно быть определено, но я просто не вижу этого. Я просто хочу закрытия!   -  person Joseph Mansfield    schedule 02.04.2011
comment
Концом вызова функции является точка последовательности (1.9.17). Поскольку результат foo() является параметром bar() (неявный this-указатель), порядок четко определен.   -  person Björn Pollex    schedule 02.04.2011
comment
@Space_C0wb0y Круто! Вот и все! Я не связывал тот факт, что результат foo() на самом деле был параметром bar(). Хотите опубликовать это как ответ, чтобы я мог принять?   -  person Joseph Mansfield    schedule 02.04.2011
comment
@ Space_C0wb0y Можно утверждать, что неявный параметр объекта и подразумеваемый аргумент объекта не существуют вне разрешения перегрузки. Эти конструкции существуют только для функций-членов, чтобы их можно было сравнить с функциями, не являющимися членами, во время разрешения перегрузки. В разделе 13 говорится: Для целей разрешения перегрузки как статические, так и нестатические функции-члены имеют неявный объектный параметр, а конструкторы — нет. Вот почему я пытаюсь вывести поведение obj.foo().bar() из другие гарантии спец. (подчеркните мое).   -  person Johannes Schaub - litb    schedule 02.04.2011
comment
Просто поясню для будущих читателей: существует большая разница между неопределенным поведением и неопределенным поведением. Undefined означает, что может случиться что угодно, и код не соответствует требованиям. Неопределенный означает, что вы можете точно сказать, что произойдет, и код соответствует требованиям, но вы не можете сказать, в каком порядке что будет происходить.   -  person John Dibling    schedule 02.04.2011


Ответы (3)


f() + g()

Здесь поведение не указано (не не определено), потому что порядок, в котором вычисляется каждый операнд (то есть вызывается каждая функция), не указан.

 obj.foo().bar();

Это хорошо определено в C++.

Соответствующий раздел §1.9.17 стандарта C++ ISO гласит:

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

Подобные случаи подробно обсуждались в этих темах:

person Nawaz    schedule 02.04.2011
comment
Спасибо за этот ответ. Я просто добавлю одну вещь: причина, по которой между вызовом foo() и вызовом bar() существует точка последовательности, заключается в том, что результат foo() передается как неявный параметр this. (Это та часть, которую я пропустил. Спасибо Space_C0wb0y за указание на это) - person Joseph Mansfield; 02.04.2011
comment
@Наваз, а как насчет a1.chain1().chain2() + a2.chain3().chain4()? Есть ли шанс, что он будет выполнен как chain1 chain3 chain4 chain2? - person rr-; 20.12.2015
comment
В частности, этот фрагмент: stream.seek(4).read_int() - stream.seek(4).read_int() должен возвращать 0, но возвращает мусор в MSVC, который указывает на то, что некоторые .seek вызываются не по порядку. За пределами этого фрагмента рассматриваемый класс потока работает очень хорошо, поэтому я ищу возможные UB. - person rr-; 20.12.2015

Если f() и g() имеют побочные эффекты для некоторого общего объекта, то поведение не определено, поскольку порядок выполнения неизвестен.

Это неправда. Вызовы функций не чередуются, и есть точка следования перед входом в функции и перед выходом из функций. Все побочные эффекты в g, соответствующие побочным эффектам в f, разделены как минимум одной точкой последовательности. Поведение не является неопределенным.

Как следствие, порядок выполнения функций f и g не определен, но как только одна функция выполняется, выполняются только оценки этой функции, а другая функция «должна ждать». Возможны разные наблюдаемые результаты, но это не означает, что произошло неопределенное поведение.

Теперь мне было интересно, что происходит, когда вы связываете функции-члены с объектом.

Если у вас есть obj.foo().bar(), вам нужно сначала оценить obj.foo(), чтобы узнать, для какого объекта вы вызываете функцию bar, что означает, что вам нужно дождаться возврата obj.foo() и получения значения. Однако это не обязательно означает, что все побочные эффекты, вызванные оценкой obj.foo(), устранены. После оценки выражения вам нужна точка последовательности, чтобы эти побочные эффекты считались завершенными. Поскольку существует точка следования перед возвратом из obj.foo(), а также перед вызовом bar(), вы фактически имеете определенный порядок выполнения побочных эффектов, инициированных оценкой выражений в foo и bar соответственно.

Чтобы объяснить немного больше, причина, по которой foo вызывается перед bar в вашем примере, такая же, как и причина, по которой i++ сначала увеличивается до вызова функции f в следующем.

int i = 0;
void f() {
  std::cout << i << std::endl;
}

typedef void (*fptype)();
fptype fs[] = { f };

int main() {
  fs[i++]();
}

Здесь следует задать вопрос: будет ли эта программа печатать 0, 1 или ее поведение не определено или не определено? Ответ таков: поскольку выражение fs[i++] обязательно должно сначала оцениваться перед вызовом функции, а перед вводом f есть точка последовательности, значение i внутри f равно 1.

Я не думаю, что вам нужно расширять область неявного параметра объекта до точек последовательности, чтобы объяснить ваш случай, и вы, конечно, не можете расширить его, чтобы объяснить этот случай (который, я надеюсь, является определенным поведением).


Черновик C++0x (в котором больше нет точек последовательности) имеет более явную формулировку этого (выделить мое)

При вызове функции (независимо от того, является ли функция встроенной) каждое вычисление значения и побочный эффект, связанные с любым выражением-аргументом, или с постфиксным выражением, обозначающим вызываемую функцию, упорядочены перед выполнением каждого выражения. или оператор в теле вызываемой функции.

person Johannes Schaub - litb    schedule 02.04.2011
comment
Но что, если побочные эффекты некоммутативны? (Это была твоя первая половина) - person Joseph Mansfield; 02.04.2011
comment
@Johannes: поведение не указано, потому что порядок, в котором оценивается каждый операнд, не указан, верно? - person Nawaz; 02.04.2011
comment
@Nawaz да, поведение не указано, но каждое возможное поведение имеет определенный результат. - person Johannes Schaub - litb; 02.04.2011
comment
Я сделал исправление «неопределенное» -> «неопределенное» в своем вопросе. Спасибо. - person Joseph Mansfield; 02.04.2011
comment
@Johannes: Мне понравился ваш пример fs[i++], чтобы объяснить §1.9.17, что теперь он делает так ясно. +1 за это. - person Nawaz; 02.04.2011
comment
@Наваз спасибо. Что я считаю важным для примера fs[i++](), так это 1.9p7, в котором говорится, что в определенных указанных точках последовательности выполнения, называемых точками последовательности, все побочные эффекты предыдущих вычислений должны быть завершены, и никаких побочных эффектов последующих вычислений не должно быть. Точка последовательности входа в функцию, описанная в 1.9p17, приводит к тому, что приращение, произведенное оценкой fs[i++], будет завершено до входа в функцию. Когда точки последовательности и 1.9p7 исчезли, спецификация больше не охватывала этот случай, и необходимо было добавить явную формулировку в 1.9p17. - person Johannes Schaub - litb; 02.04.2011

Foo() будет выполняться перед bar(). Сначала мы должны определить Foo(), иначе мы не будем знать, на что должен воздействовать bar() (мы даже не будем знать, к какому типу принадлежит класс bar(). Это может быть более интуитивно понятным для вас, если вы думаете, что Что бы мы сделали, если бы foo вернула новый экземпляр obj вместо этого или если бы foo вернула экземпляр совершенно другого класса, в котором также определен метод bar().

Вы можете проверить это самостоятельно, установив точки останова в foo и bar и посмотрев, какие из них сработают первыми.

person Kindread    schedule 02.04.2011