С++ постфиксное выражение undefined против неопределенного поведения
Извиняюсь заранее, я знаю, что общая тема оценочного порядка уже много вопросов SO. Однако, взглянув на них, я хочу прояснить несколько конкретных моментов, которые, как я думаю, не означает дублирование чего-либо. Предположим, у меня есть следующий код:
#include <iostream>
auto myLambda(int& n)
{
++n;
return [](int param) { std::cout << "param: " << param << std::endl; };
}
int main()
{
int n{0};
myLambda(n)(n);
return 0;
}
Программа выше выводит "n: 0" при компиляции. Здесь у нас есть неуказанный порядок в игре: он мог бы так же легко выводить "n: 1" имел другой порядок оценки.
Мои вопросы:
-
Что такое отношения последовательности при игре во время вызова последней функции (т.е. вызов лямбда-выражения), между постфиксным выражением myLambda(0)
, его аргументом n
и последующим вызовом функции?
-
Является ли приведенный выше пример undefined или неуказанного поведения - и почему именно (со ссылкой на стандарт)?
-
Если я изменил лямбда-код на [](int param) { std::cout << "hello" << std::endl }
(т.е. сделал результат независимым от его параметра и, следовательно, любые решения порядка оценки, сделав поведение детерминированным), ответ на 2) выше все равно будет таким же?
EDIT: я изменил имя параметра лямбда от 'n' до 'param', потому что это, казалось, вызывало путаницу.
Ответы
Ответ 1
По иронии судьбы (поскольку в этом примере используются функции С++ 11, и другие ответы были отвлечены этим) логика, которая делает этот пример неопределенным, относится к С++ 98, раздел 5, абзац 4
За исключением тех случаев, когда отмечено, порядок оценки операндов отдельных операторов и подвыражений отдельных выражений и порядок, в котором происходят побочные эффекты, не определены. Между предыдущей и следующей точкой последовательности скалярный объект должен иметь значение, которое его хранимое значение изменялось не более одного раза путем оценки выражения. Кроме того, к предыдущему значению следует обращаться только для определения значения, которое необходимо сохранить. Требования настоящего параграфа удовлетворяются для каждого допустимого упорядочения подвыражений полного выражения; в противном случае поведение undefined.
По сути, одно и то же предложение существует во всех стандартах С++, хотя, как отмечается в комментарии Марка ван Леувена, последние стандарты С++ больше не используют концепцию точек последовательности. Чистый эффект тот же: в заявлении порядок или оценка операндов операторов и подвыражений отдельных выражений остается неопределенным.
Неуказанное поведение происходит, потому что выражение n
оценивается дважды в выражении
myLambda(n)(n);
Одна оценка выражения n
(для получения ссылки) связана с первым (n)
, а другая оценка выражения n
(для получения значения) связана со вторым (n)
. Порядок оценки этих двух выражений (хотя они и оптически, как n
) не задан.
Аналогичные предложения существуют во всех стандартах С++ и имеют одинаковый результат - неопределенное поведение в выражении myLambda(n)(n)
, независимо от того, как myLambda()
реализовано
Например, myLambda()
может быть реализован в С++ 98 (и все последующие стандарты С++, включая С++ 11 и более поздние версии), подобные этому
class functor
{
functor() {};
int operator()(int n) { std::cout << "n: " << n << std::endl; };
};
functor myLambda(int &n)
{
++n;
return functor();
}
int main()
{
int n = 0;
myLambda(n)(n);
return 0;
}
так как код в вопросе - это просто (С++ 11) метод (или сокращение) для достижения того же эффекта, что и этот.
Вышеуказанные ответы на вопросы OP 1. и 2. Неуказанное поведение происходит в main()
, не связано с тем, как сам реализован myLambda()
.
Чтобы ответить на третий вопрос OP, поведение по-прежнему не указано, если лямбда (или функтор operator()
) в моем примере) изменена, чтобы не получить доступ к значению его аргумента. Единственное отличие состоит в том, что программа в целом не производит видимых результатов, которые могут отличаться от компиляторов.
Ответ 2
n
в определении лямбда является формальным аргументом функции, которую определяет лямбда. Он не имеет отношения к аргументу myLambda
, который также называется n
. Поведение здесь полностью продиктовано тем, как называются эти две функции. В myLambda(n)(n)
порядок оценки двух аргументов функции не указан. Лямбду можно вызвать с аргументом 0 или 1, в зависимости от компилятора.
Если лямбда была определена с помощью [=n]()...
, она будет вести себя по-другому.
Ответ 3
Мне не удалось найти правильную ссылку на стандарт, но я вижу, что это похоже на поведение порядка аргументов, заданное здесь, и порядок оценки аргументов функции не определен стандартом:
5.2.2 Вызов функции
8 [Примечание. Оценки постфиксного выражения и выражений аргумента не имеют никакого значения относительно друг друга. Все побочные эффекты оценок выражения аргументов секвенированы до того, как функция (см. 1.9). -end note]
Итак, как это происходит внутри вызовов на разных компиляторах:
#include <iostream>
#include <functional>
struct Int
{
Int() { std::cout << "Int(): " << v << std::endl; }
Int(const Int& o) { v = o.v; std::cout << "Int(const Int&): " << v << std::endl; }
Int(int o) { v = o; std::cout << "Int(int): " << v << std::endl; }
~Int() { std::cout << "~Int(): " << v << std::endl; }
Int& operator=(const Int& o) { v = o.v; std::cout << "operator= " << v << std::endl; return *this; }
int v;
};
namespace std
{
template<>
Int&& forward<Int>(Int& a) noexcept
{
std::cout << "Int&: " << a.v << std::endl;
return static_cast<Int&&>(a);
}
template<>
Int&& forward<Int>(Int&& a) noexcept
{
std::cout << "Int&&: " << a.v << std::endl;
return static_cast<Int&&>(a);
}
}
std::function<void(Int)> myLambda(Int& n)
{
std::cout << "++n: " << n.v << std::endl;
++n.v;
return [&](Int m) {
std::cout << "n: " << m.v << std::endl;
};
}
int main()
{
Int n(0);
myLambda(n)(n);
return 0;
}
GCC g++ -std=c++14 -O2 -Wall -pedantic -pthread main.cpp && ./a.out
и MSVC
Int (int): 0
Int (const Int &): 0
++ n: 0
Int &: 0
Int &: 0
Int (const Int &): 0
n: 0
~ Int(): 0
~ Int(): 0
~ Int(): 1
Таким образом, он создает переменную и копирует ее для перехода к возвращенной lamba.
Clang clang++ -std=c++14 main.cpp && ./a.out
Int (int): 0
++ n: 0
Int (const Int &): 1
Int &: 1
Int &: 1
Int (const Int &): 1
n: 1
~ Int(): 1
~ Int(): 1
~ Int(): 1
Здесь он создает переменную, оценивает функцию, а затем passees копирует lamba.
И порядок оценки:
struct A
{
A(int) { std::cout << "1" << std::endl; }
~A() { std::cout << "-1" << std::endl; }
};
struct B
{
B(double) { std::cout << "2" << std::endl; }
~B() { std::cout << "-2" << std::endl; }
};
void f(A, B) { }
int main()
{
f(4, 5.);
}
MSVC и GCC:
2
1
-1
-2
Clang:
1
2
-2
-1
Как и в порядке clang, он передается вперед и аргумент lambda передается после оценки аргумента функции
Ответ 4
Это поведение не указано, потому что в вызове функции (myLambda(n))(n)
выражение постфикса (которое я дал дополнительную избыточную пару круглых скобок) не зависит от выражения аргумента n
(самый правый). Однако не существует поведения undefined, потому что модификация n
происходит внутри вызова функции myLambda(n)
. Единственное определенное отношение секвенирования состоит в том, что как оценка myLambda(n)
, так и заключительная n
, очевидно, секвенированы до фактического вызова лямбда. В последнем вопросе лямбда предпочитает полностью игнорировать его аргумент, тогда поведение больше не определено: хотя неопределенно, какое значение передается как параметр, в любом случае нет различий в различии.
Ответ 5
Порядок, в котором оцениваются подвыражения, не определен и может отличаться от операторов &, ||,? а также ",".
Компилятор знает как прототипы функций myLambda, так и возвратную лямбду (извлеченную из возвращаемого типа). Потому что мое первое предложение компилятор свободен, какое выражение он оценивает первым. Вот почему вы никогда не должны вызывать функции в выражении, которые имеют дополнительные побочные эффекты.