Порядок оценки для передачи аргументов функции — порядок операций для F1(int F2(int& x), int x)

Итак, я писал код ранее. В частности, это была строка:

EnterNode( FindNode( terrain_X, terrain_Y, travel_dir ), travel_dir );

Я заметил, что после тестирования моей программы происходит что-то странное. Значение, полученное внешней функцией, не было тем значением, которое я читал, когда проверял стек.

Я сделал пример программы: https://ideone.com/wNjJrE

#include <iostream>

int modifyRef(int& A)
{
    A = 0;
    std::cout << "\nint& A = 0";
    return A;
}

void TakeValues(int X, int Y)
{
    std::cout << "\nX = " << X;
    std::cout << "\nY = " << Y;
}

int main()
{
    int Q = 9;
    TakeValues(modifyRef(Q), Q);
    std::cout << std::endl;
    system("pause");
    return 0;
}

Это вывод, который я получаю:

int& A = 0
X = 0
Y = 9

Я бы ожидал, что Y также будет равен 0. Как определяется порядок операций для привязки параметров к вызовам функций?

(Мои извинения, если я не использую правильную терминологию.)


person Josh C    schedule 02.02.2016    source источник
comment
Возможный дубликат Порядок аргументов функции в C++   -  person Chris Dodd    schedule 02.02.2016
comment
Как покажет вам тривиальный поиск, порядок оценки аргументов функции не определен ни в C, ни в C++, поэтому наличие кода, который зависит от него (как у вас), является неопределенным поведением.   -  person Chris Dodd    schedule 02.02.2016
comment
@ChrisDodd Это не указано, а не определено - ваша кошка не может забеременеть.   -  person T.C.    schedule 02.02.2016
comment
Просто замечание по стилю: поскольку код правильно использует std:: как часть имен вещей из стандартной библиотеки, using namespace std; не нужен. Кроме того, это серьезный нарушитель спокойствия.   -  person Pete Becker    schedule 02.02.2016
comment
Сформулировано по-другому, но дублирует stackoverflow .com/questions/2934904/   -  person Josh C    schedule 31.07.2019


Ответы (2)


Порядок вычисления аргументов функции не указан. Когда вы пишете:

TakeValues(modifyRef(Q), Q);

вы полагаетесь на тот факт, что modifyRef(Q) будет оцениваться до Q. Но порядок вычисления аргументов функции не указан — не обязательно, что modifyRef(Q) будет располагаться перед Q или наоборот.

В этом случае Q (второй аргумент) оценивается первым. Итак, мы читаем 9 и инициализируем им параметр Y. Затем мы оцениваем modifyRef(Q), который обнуляет Q и возвращает его, что приводит к инициализации X с 0.

person Barry    schedule 02.02.2016
comment
не может ли быть так, что Q сначала должен выполнить модификациюRef, но вызов не будет вызван до тех пор, пока не будут связаны другие экземпляры Q? Это то, что происходит, верно? Аргументы связаны, как при запуске потока с использованием параметров? - person Josh C; 02.02.2016
comment
@JoshC Единственная гарантия, которая у вас есть, заключается в том, что все аргументы будут оценены до того, как будет введено тело функции. Вы знаете, что первый Q будет прочитан до запуска modifyRef(Q), но между первым и вторым Q, а также между modifyRef() и вторым Q нет никакой гарантии. - person Barry; 02.02.2016
comment
@JoshC Например, gcc оценивает справа налево (поэтому вы получаете неожиданный результат 0/9), тогда как clang оценивает слева направо (поэтому вы получаете ожидаемый результат 0/0). Оба совершенно действительны, соответствующие результаты. Вы никогда не должны полагаться ни на то, ни на другое (поскольку любой компилятор может меняться от версии к версии). - person Barry; 02.02.2016
comment
@ Барри, я согласен; что касается Visual Studio, я считаю, что в большинстве случаев это справа налево. Учитывая исходную проблему OPS TakeValues( ModifyRef(q), q ); Здесь 2-й параметр Y устанавливается копией стека q на то, чем инициализируется q. Однако если мы изменим порядок: TakeValues( q, ModifyRef(q) ); обратите внимание на то, что здесь происходит; как X, так и Y устанавливаются на любое значение, которое ModifyRef() возвращает обратно из своего списка параметров по ссылке. Итак, в этом случае список параметров читается справа налево... - person Francis Cugler; 03.02.2016
comment
(...продолжение) Теперь рассмотрите возможность изменения обеих сигнатур функций, где ModifyRef() теперь имеет сигнатуру int& ModifyRef( int& A );, а TakeValues() теперь имеет сигнатуру void TakeValues( int& X, int& Y );. Затем вызовите TakeValues() так же, как указано выше, где параметры находятся в обратном порядке; здесь это не справа налево и не слева направо, или, по крайней мере, это не очевидно, потому что внутренняя функция, независимо от ее местоположения, оценивается первой, и как только она возвращает измененную ссылку q, теперь устанавливает X и Y в TakeValues(). - person Francis Cugler; 03.02.2016
comment
@FrancisCugler Порядок оценки аргументов в функциях не указан. Я не знаю, о чем вы говорите или почему вы пытаетесь определить порядок. Порядок отсутствует, и вы не должны писать код, который полагается на его наличие. - person Barry; 03.02.2016
comment
@Barry Я не зависим от компилятора, я просто указываю, о чем нужно знать, и тот факт, что я согласен с вами. Я просто объяснял результаты того, что кажется порядком при использовании Visual Studio в моем случае. Не существует определенного стандартного порядка; он полностью зависит от компилятора и может меняться от версии к версии. - person Francis Cugler; 03.02.2016
comment
Ну, если быть честным здесь. Я спросил, как это определяется, я спросил не о стандарте, а о практических знаниях (по крайней мере, это было моим намерением). Не указано или нет, должен быть заказ. - person Josh C; 04.02.2016
comment
@JoshC И я ответил на этот вопрос. Вы должны писать код без каких-либо предположений об упорядочении, потому что вы не можете их сделать. - person Barry; 04.02.2016
comment
@Barry: Есть разница между неуказанным порядком и отсутствием порядка (неупорядоченного). Если глобальная переменная unsigned i изначально равна 4, выражения i++; и i-- оцениваются в порядке Unspecified, и ничто другое не изменяет i, i в конечном итоге будет иметь исходное значение. Однако, если операции не упорядочены, может случиться что угодно. - person supercat; 30.03.2016
comment
@supercat Я не понимаю цель вашего комментария? - person Barry; 30.03.2016

Попробуйте изменить подпись ваших функций:

int ModifyRef( int& A );

void TakeValues( int X, int Y );

к следующему...

int& ModifyRef( int& A );

void TakeValues( int& X, int& Y );

и посмотрите, каким будет ваш вывод в main, когда вы вызовете эту строку кода:

int q = 9;

TakeValues( ModifyRef(q), q );

затем измените порядок параметров как таковой

TakeValues( q, ModifyRef(q) );

и сравните результаты.

Когда я сделал это на своей машине Win 7 64bit с сообществом VS2015 на Intel Core2 Quad Extreme, результаты, которые я получил для обеих ситуаций, когда использование ссылок было одинаковым, и результат, который я получаю, таков:

Я изменил значение A в ModifyRef() с 0 на 7 в целях тестирования.

int& A = 7
X = 7
Y = 7

для обеих ситуаций, когда либо внутренняя функция является первым параметром, а автономная переменная является вторым, либо внутренняя функция является вторым параметром, а автономная переменная является первой.

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

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

int& ModifyRef( int& A ) {
    A = 7; // Changed Value From 0 To 7 For Testing Purposes
    std::cout << "n\int& A = " << A;
    return A;
}

Однако этот эффект может применяться только при использовании только ссылок.

Когда я возвращаюсь к вашим исходным сигнатурам функций и вызываю их в следующем порядке:

q = 9;
TakeValues( ModifyRef( q ), q );

Поскольку ваш исходный код был представлен, я получаю вывод:

int& A = 7
X = 7
Y = 9

Однако, когда я изменяю параметры на:

q = 9;
TakeValues( q, ModifyRef( q ) );

Мой вывод:

int& A = 7
X = 7
Y = 7

Итак, то, что я вижу в этих двух ситуациях, немного отличается.

В первом порядке параметров Y или второму параметру присваивается значение 9, поскольку q инициализируется значением 9, а Y внутри TakeValues() печатает копию стека со значением 9. Затем X оценивается ModifyRef(), где q имеет значение 9, но затем оно изменяется, поскольку оно является ссылкой, поэтому, когда TakeValues() устанавливает X из q, q уже было изменено с 9 на 7, поэтому X теперь устанавливается на 7.

Во втором порядке параметров видно, что ModifyRef() вызывается первым и изменяет q с 9 на 7, поэтому TakeValues() устанавливает Y на 7, и, поскольку эта функция использует ссылку, q также было изменено с 9 на 7, поэтому, когда первый параметр принимает q для установки X q уже изменено с 9 на 7.

Я не знаю, зависит ли это от компилятора, но, по крайней мере, на моей машине кажется, что стек вызовов для параметров происходит от самого дальнего справа налево. Это также имеет смысл, когда вы думаете об этом из-за того, что функции имеют значения по умолчанию.

Пример:

class foo {
    void bar( int a, int b, int c = 3 ) {
        std::cout << a << ", " << b << ", " << c << std::endl;
    }
};

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

class foo {
   void bar( int a = 1, int b, int c ) { ... } // Compile Error
};

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

person Francis Cugler    schedule 02.02.2016
comment
Отличный ответ! Я даже не думал, что изменение порядка параметров может иметь такой эффект. Я начал использовать const & для моего travel_dir в своей исходной задаче. Я могу вернуться и изменить порядок параметров. - person Josh C; 02.02.2016
comment
Бывают случаи, когда вы должны использовать const, а когда нет. Если вы знаете, что значение не должно меняться, потому что оно всегда будет постоянным, например, PI или e, то оно должно быть постоянным значением. Если значение должно обрабатываться каким-либо математическим оператором, оно должно передаваться либо по значению, либо по ссылке. Кроме того, если функции нужен только указанный набор данных для принятия решения, то const ref может работать, но если ей нужно передать значения через свой список параметров после выполнения вычислений, от которых зависят другие функции, const ref не будет работать. - person Francis Cugler; 03.02.2016
comment
Еще одна вещь, которая поможет при написании функций, принадлежащих объектам класса, заключается в том, что если вы знаете, что функция не должна изменять значения, вы должны добавить функцию с константой: скажем, это принадлежит некоторому классу A, float getValue() const; обычно с геттерами, которые просто возвращают переменная-член, функция объявлена ​​как константа, чтобы не изменять никаких внутренних значений. Существует несколько различных вариантов использования const. - person Francis Cugler; 03.02.2016