Точки последовательности и метод
Следующее выражение часто используется для демонстрации неуказанного поведения undefined:
f() + g()
Если f()
и g()
имеют побочные эффекты на каком-то общем объекте, тогда поведение undefined не указано, потому что порядок выполнения неизвестен. f()
может быть оценен до g()
или наоборот.
Теперь мне было интересно, что происходит, когда вы цепляете функции-члены за объект. Скажем, у меня есть экземпляр класса, экземпляр с именем obj
и он имеет две функции-члена, foo()
и bar()
, которые оба изменяют объект. Порядок выполнения этих функций не является коммутативным. Эффект вызова одного перед другим - это не тот же эффект, что и вызов другим способом. Оба метода возвращают ссылку на *this
, чтобы они могли быть скованы так:
obj.foo().bar()
Но неужели это неуказанное поведение? Я не могу найти ничего в стандарте (по общему признанию, просто просматривая), который различает это выражение и выражение, которое я дал в верхней части сообщения. Оба вызова функций являются подвыражениями полного выражения, поэтому их порядок выполнения не указан. Но, конечно, foo()
должен оцениваться первым, чтобы bar()
знал, какой объект изменить.
Возможно, мне не хватает чего-то очевидного, но я не вижу, где создается точка последовательности.
Ответы
Ответ 1
f() + g()
Здесь поведение неуказано (не undefined), потому что порядок, в котором оценивается каждый операнд (то есть каждая функция), не указан.
obj.foo().bar();
Это хорошо определено в С++.
Соответствующий раздел §1.9.17 из стандарта С++ ISO гласит:
При вызове функции (или не функция является встроенной), есть точка после оценки всех аргументов функции (если есть) которое происходит до любые выражения или утверждения в функция корпус. Существует также точка последовательности после копирования возвращаемое значение и до выполнение любых выражений вне функция.
Подобные случаи обсуждались в деталях в следующих разделах:
Ответ 2
Если f() и g() имеют побочные эффекты на каком-то общем объекте, тогда поведение undefined, потому что порядок выполнения неизвестен.
Это неверно. Вызовы функций не чередуются, и перед входом функций и перед оставлением функций есть точка последовательности. Все побочные эффекты в g
, соответствующие побочным эффектам в f
, разделяются по меньшей мере одной точкой последовательности. Поведение не undefined.
Как следствие, порядок выполнения функций f
и g
не определен, но как только одна функция выполняется, выполняются только оценки функций, а другая функция "должна ждать". Возможны различные наблюдаемые результаты, но это не означает, что поведение undefined произошло.
Теперь мне было интересно, что происходит, когда вы связываете функции-члены на объекте.
Если у вас есть 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
или ее поведение undefined или неуказано? Ответ заключается в том, что выражение fs[i++]
обязательно должно быть сначала оценено перед вызовом функции, и перед входом f
есть точка последовательности, значение i
внутри f
равно 1
.
Я не думаю, что вам нужно расширить сферу неявного параметра объекта на точки последовательности, чтобы объяснить ваше дело, и вы, конечно же, не можете продлить его, чтобы объяснить этот случай (который, я надеюсь, определяется поведением).
Проект С++ 0x (который больше не имеет точек последовательности) имеет более явную формулировку этого (подчеркивайте мой)
При вызове функции (независимо от того, является ли функция встроенной) каждое вычисление значения и побочный эффект, связанный с любым выражением аргумента, или с выражением postfix, обозначающим вызываемую функцию, секвенированы перед выполнением каждого выражения или выражения в теле вызываемой функции.
Ответ 3
Foo() будет выполняться перед bar(). Мы должны сначала определить Foo(), иначе мы не будем знать, над чем должен действовать bar() (мы даже не знаем, к какому классу класса bar() принадлежит. Это может быть более интуитивно понятным для вас, если вы думаете, что мы должны были бы сделать, если foo вернул новый экземпляр obj вместо этого. Или если foo возвратил экземпляр совершенно другого класса, который также имеет определенный bar() метод.
Вы можете проверить это самостоятельно, используя точки останова в foo и bar и увидев, что попадает в первую очередь.