Порядок выполнения C ++ в цепочке методов

108

Результат этой программы:

#include <iostream> 
class c1
{   
  public:
    c1& meth1(int* ar) {
      std::cout << "method 1" << std::endl;
      *ar = 1;
      return *this;
    }
    void meth2(int ar)
    {
      std::cout << "method 2:"<< ar << std::endl;
    }
};

int main()
{
  c1 c;
  int nu = 0;
  c.meth1(&nu).meth2(nu);
}

Является:

method 1
method 2:0

Почему nuне 1 при meth2()запуске?

Мойзес Виньяс
источник
42
@MartinBonner: Хотя я знаю ответ, я бы не назвал его «очевидным» в любом смысле этого слова, и даже если бы это было так, это не было бы достойной причиной для того, чтобы пренебрегать голосом. Неутешительно!
Гонки легкости на орбите
4
Это то, что вы получите, если измените свои аргументы. Функции, изменяющие свои аргументы, труднее читать, их эффекты неожиданны для следующего программиста, работающего над кодом, и приводят к таким сюрпризам. Я настоятельно рекомендую избегать изменения каких-либо параметров, кроме вызывающего. Изменение инвоканта здесь не будет проблемой, потому что второй метод вызывается по результату первого, поэтому эффекты упорядочиваются по нему. Однако есть еще случаи, когда их не было бы.
Ян Худек
@JanHudec Именно поэтому функциональное программирование уделяет так много внимания чистоте функций.
Pharap
2
Например, соглашение о вызовах на основе стека, вероятно, предпочтет передать nu, &nuи cдалее в стек в этом порядке, затем вызвать meth1, отправить результат в стек, а затем вызвать meth2, в то время как соглашение о вызовах на основе регистров захочет загрузить cи &nuв регистры, вызвать meth1, загрузить nuв регистр, затем вызвать meth2.
Нил

Ответы:

66

Поскольку порядок оценки не указан.

Вы видите nuв mainнастоящее время оценивается в 0еще до того , meth1как называется. Это проблема с цепочкой. Советую этого не делать.

Просто сделайте красивую, простую, понятную, легкую для чтения и понятную программу:

int main()
{
  c1 c;
  int nu = 0;
  c.meth1(&nu);
  c.meth2(nu);
}
Гонки легкости на орбите
источник
14
Есть вероятность, что предложение об уточнении порядка оценки в некоторых случаях , которое устраняет эту проблему, появится для C ++ 17
Revolver_Ocelot
7
Мне нравится объединение методов (например, <<для вывода и «построители объектов» для сложных объектов со слишком большим количеством аргументов для конструкторов - но это очень плохо сочетается с выходными аргументами.
Мартин Боннер поддерживает Монику
34
Я правильно понимаю? порядок оценки meth1и meth2определен, но оценка параметра meth2может произойти раньше meth1...?
Родди
7
Цепочка методов хороша, пока методы разумны и изменяют только вызывающий (для которого эффекты хорошо упорядочены, потому что второй метод вызывается в результате первого).
Ян Худек
4
Это логично, если подумать. Работает вродеmeth2(meth1(c, &nu), nu)
BartekChom
29

Я думаю, что эта часть проекта стандарта, касающаяся порядка оценки, актуальна:

1.9 Выполнение программы

...

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

а также:

5.2.2 Вызов функции

...

  1. [Примечание: все оценки постфиксного выражения и аргументов не упорядочены относительно друг друга.Все побочные эффекты оценок аргументов упорядочиваются перед вводом функции - конец примечания]

Итак, для вашей строки c.meth1(&nu).meth2(nu);рассмотрите, что происходит в operator с точки зрения оператора вызова функции для последнего вызова meth2, чтобы мы четко видели разбивку на постфиксное выражение и аргументnu :

operator()(c.meth1(&nu).meth2, nu);

В оценках выражения постфикса и аргумент для вызова функции конечного (то есть выражение постфикса c.meth1(&nu).meth2и nu) являются unsequenced относительно друг друга в соответствии с вызовом функции правило , выше. Следовательно, побочный эффект вычисления постфиксного выражения на скалярном объекте arне упорядочен по сравнению с оценкой аргумента nuдо meth2вызова функции. По приведенному выше правилу выполнения программы это неопределенное поведение.

Другими словами, компилятору не требуется оценивать nuаргумент meth2вызова после meth1вызова - он может предполагать отсутствие побочных эффектов, meth1влияющих на nuоценку.

Ассемблерный код, созданный выше, содержит в функции следующую последовательность main:

  1. Переменная nuразмещается в стеке и инициализируется 0.
  2. Регистр ( ebxв моем случае) получает копию значенияnu
  3. Адреса nuи cзагружаются в регистры параметров.
  4. meth1 называется
  5. Регистр возвращаемого значения , и ранее кэшированное значение из nuв ebxрегистре загружаются в регистры параметров
  6. meth2 называется

Важно отметить, что на шаге 5 выше компилятор позволяет nuповторно использовать кэшированное значение из шага 2 в вызове функции meth2. Здесь игнорируется возможность, которая nuмогла быть изменена призывом к meth1«неопределенному поведению» в действии.

ПРИМЕЧАНИЕ. Этот ответ по существу изменился по сравнению с исходной формой. Мое первоначальное объяснение с точки зрения побочных эффектов вычисления операндов, которые не были упорядочены до последнего вызова функции, было неверным, потому что они есть. Проблема заключается в том, что вычисление самих операндов имеет неопределенную последовательность.

Смихи
источник
2
Это не верно. Вызовы функций неопределенно упорядочены с другими вычислениями в вызывающей функции (если иное не наложено ограничение «Последовательность до»); они не чередуются.
TC
1
@TC - я никогда ничего не говорил о чередовании вызовов функций. Я упомянул только побочные эффекты операторов. Если вы посмотрите на ассемблерный код, созданный вышеупомянутым, вы увидите, что meth1он выполняется раньше meth2, но параметр для meth2- это значение, nuкэшированное в регистр перед вызовом, meth1то есть компилятор проигнорировал потенциальные побочные эффекты, которые в соответствии с моим ответом.
Smeeheey
1
Вы в точности утверждаете, что - «его побочный эффект (т.е. установка значения ar) не гарантированно будет упорядочен до вызова». Оценка постфиксного выражения в вызове функции (что есть c.meth1(&nu).meth2) и оценка аргумента этого call ( nu) обычно неупорядочены, но 1) все их побочные эффекты упорядочиваются перед входом в meth2и 2) поскольку c.meth1(&nu)это вызов функции , он имеет неопределенную последовательность с вычислением nu. Внутри meth2, если он каким-то образом получит указатель на переменную in main, он всегда будет видеть 1.
TC
2
«Однако побочный эффект вычисления операндов (т. Е. Установка значения ar) не гарантированно будет упорядочен перед чем-либо вообще (согласно пункту 2 выше)». Абсолютно гарантированная последовательность перед вызовом meth2, как указано в пункте 3 цитируемой страницы cppreference (которую вы также не процитировали должным образом).
TC
1
Вы ошиблись и сделали еще хуже. Здесь нет абсолютно никакого неопределенного поведения. Продолжайте читать [intro.execution] / 15, за исключением примера.
TC
9

В стандарте C ++ 1998 г., раздел 5, пункт 4

Если не указано иное, порядок оценки операндов отдельных операторов и подвыражений отдельных выражений, а также порядок, в котором имеют место побочные эффекты, не указан. Между предыдущей и следующей точкой последовательности скалярный объект должен иметь свое сохраненное значение, измененное не более одного раза при оценке выражения. Кроме того, к предыдущему значению необходимо обращаться только для определения значения, которое необходимо сохранить. Требования данного параграфа должны соблюдаться для каждого допустимого порядка подвыражений полного выражения; в противном случае поведение не определено.

(Я пропустил ссылку на сноску № 53, которая не имеет отношения к этому вопросу).

По сути, &nuдолжен быть оценен перед вызовом c1::meth1()и nuдолжен быть оценен перед вызовом c1::meth2(). Однако нет никаких требований, которые nuоценивались бы раньше &nu(например, разрешается nuсначала оценивать, затем &nu, а затем c1::meth1()вызывать - что может быть тем, что делает ваш компилятор). Выражение *ar = 1в c1::meth1()поэтому не гарантируется быть оценены , прежде чем nuв main()оценивается, чтобы быть переданы c1::meth2().

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

Питер
источник
7

Я думаю, что при компиляции, до того, как на самом деле будут вызваны функции meth1 и meth2, им были переданы параметры. Я имею в виду, когда вы используете "c.meth1 (& nu) .meth2 (nu);" значение nu = 0 было передано в meth2, поэтому не имеет значения, будет ли изменено "nu" позже.

вы можете попробовать это:

#include <iostream> 
class c1
{
public:
    c1& meth1(int* ar) {
        std::cout << "method 1" << std::endl;
        *ar = 1;
        return *this;
    }
    void meth2(int* ar)
    {
        std::cout << "method 2:" << *ar << std::endl;
    }
};

int main()
{
    c1 c;
    int nu = 0;
    c.meth1(&nu).meth2(&nu);
    getchar();
}

он получит ответ, который вы хотите

Футболка saintor
источник