Подтвердить что ты не робот

С++ постфиксное выражение 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', потому что это, казалось, вызывало путаницу.

4b9b3361

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