Порядок виконання 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()починається?


41
@MartinBonner: Хоча я знаю відповідь, я б не називав це "очевидним" у будь-якому сенсі цього слова, і навіть якби це було, це не було б гідною причиною для того, щоб проїхати по каналу. Розчарування!
Гонки легкості по орбіті

4
Це ви отримуєте, змінюючи свої аргументи. Функції, що змінюють їхні аргументи, важче читати, їх ефекти несподівані для наступного програміста, що працює над кодом, і призводять до таких сюрпризів. Я настійно пропоную уникати змін будь-яких параметрів, окрім інкабанта. Зміна виклику тут не складе жодних проблем, оскільки другий метод викликається результатом першого, тому ефекти впорядковані на ньому. Є ще деякі випадки, коли їх не було б.
Ян Худець


@JanHudec Саме тому функціональне програмування робить такий великий акцент на чистоті функції.
Фарап

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);розгляньте, що відбувається в операторі з точки зору оператора виклику функції для остаточного виклику 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
Це неправильно. Функціональні виклики нескінченно секвенуються без інших оцінок функції виклику (якщо інше не встановлено послідовність попереднього обмеження); вони не переплітаються.
ТК

1
@TC - я ніколи нічого не говорив про переплутування викликів функцій. Я згадував лише побічні ефекти операторів. Якщо ви подивитеся на код складання, створений вище, ви побачите, що meth1він виконується раніше meth2, але параметр для meth2- це значення, nuкешоване в регістр перед викликом до meth1- тобто компілятор ігнорував потенційні побічні ефекти, які є відповідає моїй відповіді.
Smeeheey

1
Ви точно стверджуєте, що - "його побічний ефект (тобто встановлення значення ar) не гарантується, що він буде секвенсований перед викликом". Оцінка експресії постфікса у виклику функції (яка є c.meth1(&nu).meth2) та оцінка аргументу цього виклику ( nu), як правило, не є наслідком, але 1) всі їх побічні ефекти послідовні перед входом у meth2та 2), оскільки c.meth1(&nu)це виклик функції , це невизначено послідовно з оцінкою nu. Всередині meth2, якби якимось чином отримав вказівник на змінну в main, він завжди побачив би 1.
ТК

2
"Однак побічний ефект обчислення операндів (тобто встановлення значення ar) не гарантується, що він буде секвенуватися перед чим-небудь взагалі (відповідно до 2) вище)." Абсолютно гарантовано провести послідовність перед викликом meth2, як зазначено в пункті 3 сторінки cppreference, яку ви цитуєте (яку ви також знехтували належним чином цитувати).
ТЦ

1
Ви взяли щось не так і погіршили. Тут немає абсолютно невизначеної поведінки. Продовжуйте читати [intro.execution] / 15, минаючи приклад.
ТК

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

він отримає потрібну відповідь

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.