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

Вывод этой программы:

#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);
}

Is:

method 1
method 2:0

Почему nu не 1, когда начинается meth2()?


person Moises Viñas    schedule 16.05.2016    source источник
comment
@MartinBonner: Хотя я знаю ответ, я бы не назвал его очевидным в любом смысле этого слова, и даже если бы это было так, это не было бы веской причиной для того, чтобы проголосовать против. Разочаровывает!   -  person Lightness Races in Orbit    schedule 16.05.2016
comment
Это то, что вы получаете, когда изменяете свои аргументы. Функции, изменяющие свои аргументы, труднее читать, их эффекты неожиданны для следующего программиста, работающего над кодом, и они приводят к подобным сюрпризам. Я настоятельно рекомендую избегать изменения каких-либо параметров, кроме invocant. Изменение инвоканта здесь не будет проблемой, потому что второй метод вызывается на основе результата первого, поэтому эффекты упорядочиваются на нем. Однако есть случаи, когда их не было бы.   -  person Jan Hudec    schedule 16.05.2016
comment
Это также актуальный вопрос   -  person Shafik Yaghmour    schedule 17.05.2016
comment
@JanHudec Именно поэтому функциональное программирование уделяет такое большое внимание чистоте функций.   -  person Pharap    schedule 17.05.2016
comment
Например, соглашение о вызовах на основе стека, вероятно, предпочтет помещать nu, &nu и c в стек в указанном порядке, затем вызывать meth1, помещать результат в стек, затем вызывать meth2, в то время как основанное на регистрах соглашение о вызовах требует загрузки c и &nu в регистры, вызова meth1, загрузки nu в регистр, а затем вызова meth2.   -  person Neil    schedule 17.05.2016
comment
Ответ на этот вопрос зависит от стандарта C++. Начиная с C++17, он изменился с P0145 принято в спец. См. также: это сообщение SO, cppref по теме и этот момент в презентации CoreCpp 2019.   -  person Amir Kirsh    schedule 07.06.2021


Ответы (5)


Потому что порядок оценки не указан.

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

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

int main()
{
  c1 c;
  int nu = 0;
  c.meth1(&nu);
  c.meth2(nu);
}
person Lightness Races in Orbit    schedule 16.05.2016
comment
Существует вероятность, что предложение уточнить порядок оценки в некоторых случаях, который устраняет эту проблему, подходит для C++17. - person Revolver_Ocelot; 16.05.2016
comment
Мне нравится цепочка методов (например, << для вывода и построители объектов для сложных объектов со слишком большим количеством аргументов для конструкторов, но они очень плохо сочетаются с выходными аргументами. - person Martin Bonner supports Monica; 16.05.2016
comment
Я правильно понимаю? порядок оценки meth1 и meth2 определен, но оценка параметра для meth2 может произойти до вызова meth1...? - person Roddy; 16.05.2016
comment
Цепочка методов хороша, если методы разумны и изменяют только вызывающий объект (для которого эффекты хорошо упорядочены, потому что второй метод вызывается на основе результата первого). - person Jan Hudec; 16.05.2016
comment
Теперь, чтобы быть строго эквивалентным, это должно быть tmp = c.meth1(&nu); tmp.meth2(nu);, верно? Поскольку meth1 возвращает *this, в этом случае они будут эквивалентны, но если бы c.meth возвращал что-то необычное, это не было бы одинаковым. На самом деле это просто уточнение комментария Яна Худека о том, что второй метод вызывается в результате первого. - person Joshua Taylor; 16.05.2016
comment
@JoshuaTaylor: Да, если бы программа была другой, вам пришлось бы писать другой код. - person Lightness Races in Orbit; 16.05.2016
comment
Обратите внимание, что хотя параметр meth2 может или не может оцениваться до вызова meth1 в соответствии со стандартом, в данном конкретном случае и с оптимизирующим компилятором очень вероятно, что он будет оцениваться первым, просто потому, что компилятор знает, каково его значение в начале оператора, и поэтому может сохранить инструкцию загрузки памяти, вычислив ее в первую очередь. - person Jules; 16.05.2016
comment
Это логично, если подумать. Это работает как meth2(meth1(c, &nu), nu) - person BartekChom; 17.05.2016
comment
@Buksy: Тогда хорошо, что ваш учебник C ++ объясняет это явление. - person Lightness Races in Orbit; 18.05.2016
comment
@LightnessRacesinOrbit не уверен, что понял, какой учебник вы имеете в виду? - person Buksy; 18.05.2016

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

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

...

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

а также:

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

...

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

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

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

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

Другими словами, компилятору не требуется оценивать аргумент 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 — «неопределенное поведение» в действии.

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

person Smeeheey    schedule 16.05.2016
comment
На практике происходит то, что оценка аргумента meth2 происходит до вызова meth1. Примечание. Ваш анализ последовательности верен, но моя формулировка может быть проще для ОП. - person Martin Bonner supports Monica; 16.05.2016
comment
Это не правильно. Вызовы функций упорядочиваются в неопределенной последовательности по отношению к другим вычислениям в вызывающей функции (если иное не наложено ограничение последовательности перед); они не чередуются. - person T.C.; 17.05.2016
comment
@Т.С. - Я ничего не говорил о чередовании вызовов функций. Я говорил только о побочных эффектах операторов. Если вы посмотрите на ассемблерный код, созданный выше, вы увидите, что meth1 выполняется до meth2, но параметр для meth2 представляет собой значение nu, кэшированное в регистре до вызова meth1, т.е. компилятор проигнорировал потенциальную сторону -эффекты, что согласуется с моим ответом. - person Smeeheey; 17.05.2016
comment
Вы точно утверждаете, что его побочный эффект (т.е. установка значения ar) не обязательно будет упорядочен до вызова. Оценка постфиксного выражения в вызове функции (то есть c.meth1(&nu).meth2) и оценка аргумента этого вызова (nu) обычно не упорядочены, но 1) все их побочные эффекты упорядочены до входа в meth2 и 2) поскольку c.meth1(&nu) вызов функции, он неопределенно упорядочен с оценкой nu. Внутри meth2, если он каким-то образом получит указатель на переменную в main, он всегда будет видеть 1. - person T.C.; 17.05.2016
comment
@Т.С. - Я не думаю, что утверждал что-то, что противоречило бы тому, что вы говорите. В любом случае, я отредактировал ответ, чтобы он был несколько более подробным. Надеюсь, теперь вы согласитесь с этим. - person Smeeheey; 17.05.2016
comment
Однако не гарантируется, что побочный эффект вычисления операндов (т. е. установка значения ar) будет упорядочен перед чем-либо вообще (согласно 2) выше). Он абсолютно гарантированно будет упорядочен перед вызовом meth2, как указано в пункте 3 страницы cppreference, которую вы цитируете (которую вы также забыли правильно процитировать). - person T.C.; 17.05.2016
comment
@Т.С. - Вам трудно угодить :) . Хорошо, я сделал еще 2 незначительных правки. Я понимаю, о чем вы говорите, но сейчас это кажется довольно педантичным. Да, хорошо, что-то вообще было, строго говоря, неверным, но я думаю, что из остального контекста ясно, что все, о чем идет речь, - это оценка всех операндов. - person Smeeheey; 18.05.2016
comment
@Т.С. - Я дополнительно сильно отредактировал большую часть ответа, в том числе удалил ссылку cppreference, которая не объясняет, что здесь происходит. Спасибо за ваш вклад, я думаю, что ответ теперь намного точнее из-за этого. - person Smeeheey; 19.05.2016
comment
Вы взяли что-то не так и сделали еще хуже. Здесь нет абсолютно никакого неопределенного поведения. Продолжайте читать [intro.execution]/15 после примера. - person T.C.; 19.05.2016
comment
Я читал дальше, что конкретно вы имеете в виду? Вы рады поговорить об этом в приватном чате? - person Smeeheey; 19.05.2016
comment
Посмотрите на ассемблерный код, полученный выше. Компилятор имел полное право загружать nu из стека в регистр параметров после вызова meth1, что привело бы к другому результату. Как это может быть четко определенным поведением? - person Smeeheey; 19.05.2016
comment
Каждое вычисление в вызывающей функции (включая вызовы других функций), которое иначе не упорядочено особым образом до или после выполнения тела вызываемой функции, имеет неопределенную последовательность по отношению к выполнению вызываемой функции. (Формулировка изменена в текущем рабочем черновике, но суть не изменилась.) Вычисление nu-аргумента-к-meth2 и c.meth1(&nu) упорядочены неопределенно, а не неупорядочены, поэтому поведение не указано, а не не определено. Эти двое — отдельный мир. - person T.C.; 19.05.2016
comment
Оценка nu-аргумента-к-meth2 и c.meth1(&nu) имеет неопределенную последовательность - этот вывод не подтверждается приведенной вами цитатой. На самом деле цитата показывает, что обе они имеют неопределенную последовательность относительно выполнения вызываемой функции (т.е. meth2). Это ничего не говорит о том, что они неопределенно упорядочены по отношению друг к другу. - person Smeeheey; 20.05.2016
comment
Здесь у вас есть две вызываемые функции. meth1 - это каждый бит функции, как meth2. (Оценки &nu и nu-аргумент-к-meth2 не упорядочены, но вызов meth1 неопределенно упорядочен по отношению к каждой другой оценке в main, включая оценку nu-аргумента-к-meth2 .) - person T.C.; 22.05.2016

В стандарте 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++ (которых у меня в настоящее время нет на ПК, который я использую сегодня вечером) имеют по сути то же самое предложение.

person Peter    schedule 16.05.2016

Я думаю, что при компиляции до того, как действительно вызываются функции 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();
}

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

person T-shirt saintor    schedule 16.05.2016

Ответ на этот вопрос зависит от стандарта C++.

Правила изменились по сравнению с C++17 с P0145 принято в спецификации. Начиная с C++17, порядок оценки определен, и оценка параметров будет выполняться в соответствии с порядком вызовов функций. Обратите внимание, что порядок оценки параметров внутри одного вызова функции по-прежнему не указан.

Таким образом, порядок оценки в выражениях цепочки гарантируется, начиная с С++ 17, для работы в фактическом порядке цепочки: рассматриваемый код гарантируется, начиная с С++ 17, для печати:

method 1
method 2:1

До C++17 он мог печатать приведенное выше, но также мог печатать:

method 1
method 2:0

Смотрите также:

person Amir Kirsh    schedule 07.06.2021