C++后缀表达式未定义与未指定的行为

Sme*_*eey 24 c++ operator-precedence undefined-behavior language-lawyer c++14

提前道歉,我知道评估订单的一般主题已经有很多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;
}
Run Code Online (Sandbox Code Playgroud)

上面的程序在编译时输出"n:0".在这里我们有未指定的排序:它可以很容易地输出"n:1"发生了不同的评估顺序.

我的问题是:

  1. 在上面的最终函数调用(即lambda表达式调用)中,后缀表达式myLambda(0),其参数n和后续函数调用本身之间的排序关系究竟是什么?

  2. 以上是未定义或未指定行为的示例- 以及为什么(参考标准)?

  3. 如果我将lambda代码更改为[](int param) { std::cout << "hello" << std::endl }(即使结果独立于其参数,因此任何评估顺序决策,使行为确定性),上述2)的答案是否仍然相同?

编辑:我已将lambda参数名称从'n'更改为'param',因为这似乎导致了混淆.

Pet*_*ter 17

具有讽刺意味的是(由于该示例使用了C++ 11特性,而其他答案已被分散注意力)使得此样本具有未指定行为的逻辑可追溯到C++ 98,第5节,第4段

除非另有说明,否则单个运算符的操作数和单个表达式的子表达式的评估顺序以及副作用发生的顺序是未指定的.在前一个和下一个序列点之间,标量对象应通过表达式的计算最多修改其存储值一次.此外,只能访问先前值以确定要存储的值.对于完整表达式的子表达式的每个允许排序,应满足本段的要求; 否则行为未定义.

基本上相同的条款存在于所有C++标准中,尽管正如Marc van Leeuwen的评论中所指出的,最近的C++标准不再使用序列点的概念.净效应是相同的:在一个语句中,运算符的操作数和单个表达式的子表达式的顺序或评估仍未指定.

出现未指定的行为是因为n在语句中对表达式求值两次

myLambda(n)(n);
Run Code Online (Sandbox Code Playgroud)

表达式的一个评估n(以获得参考)与第一个相关联,(n)并且表达式的另一个评估n(以获得值)与第二个相关联(n).这两个表达式的评估顺序(即使它们在光学上都是两者n)都未指定.

ALL C++标准中存在类似的子句,并且具有相同的结果 - 语句上的未指定行为myLambda(n)(n),无论如何myLambda()实现

例如,myLambda()可以在C++ 98(以及所有后来的C++标准,包括C++ 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;
 }
Run Code Online (Sandbox Code Playgroud)

因为问题中的代码只是一种(C++ 11)技术(或简写)来实现与此相同的效果.

以上回答OP的问题1.和2.未指明的行为发生在main(),与其myLambda()实现方式无关.

要回答OP的第三个问题,如果修改了lambda(或operator()我的示例中的functor )而不访问其参数的值,则行为仍未指定.唯一的区别是程序作为一个整体产生的可见输出可能会因编译器而异.


tei*_*vaz 2

我没有找到对标准的正确参考,但我发现它与此处询问的参数评估顺序有类似的行为询问的参数评估顺序具有类似的行为,并且标准未定义函数参数评估的顺序:

\n\n
\n

5.2.2 函数调用

\n\n

8 [注意:后缀表达式和参数表达式的计算相对于彼此都是无序的。参数表达式求值的所有副作用都会在输入函数之前排序(参见 1.9)。\xe2\x80\x94结束注]

\n
\n\n

下面是不同编译器上的调用内部的情况:

\n\n
#include <iostream>\n#include <functional>\n\nstruct Int\n{\n    Int() { std::cout << "Int(): " << v << std::endl; }\n    Int(const Int& o) { v = o.v; std::cout << "Int(const Int&): " << v << std::endl; }\n    Int(int o) { v = o; std::cout << "Int(int): " << v << std::endl; }\n    ~Int() { std::cout << "~Int(): " << v << std::endl; }\n    Int& operator=(const Int& o) { v = o.v; std::cout << "operator= " << v << std::endl; return *this; }\n\n    int v;\n};\n\nnamespace std\n{\n    template<>\n    Int&& forward<Int>(Int& a) noexcept\n    {\n        std::cout << "Int&: " << a.v << std::endl;\n        return static_cast<Int&&>(a);\n    }\n\n    template<>\n    Int&& forward<Int>(Int&& a) noexcept\n    {\n        std::cout << "Int&&: " << a.v << std::endl;\n        return static_cast<Int&&>(a);\n    }\n}\n\nstd::function<void(Int)> myLambda(Int& n)\n{\n    std::cout << "++n: " << n.v << std::endl;\n    ++n.v;\n    return [&](Int m) { \n        std::cout << "n: " << m.v << std::endl;\n    };\n}\n\nint main()\n{\n    Int n(0);\n\n    myLambda(n)(n);\n    return 0;\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

海湾合作委员会g++ -std=c++14 -O2 -Wall -pedantic -pthread main.cpp && ./a.out和MSVC

\n\n
\n

Int(int): 0
\n Int(const Int&): 0
\n ++n: 0
\n Int&: 0
\n Int&: 0
\n Int(const Int&): 0
\nn: 0
\n ~Int( ): 0
\n ~Int(): 0
\n ~Int(): 1

\n
\n\n

因此它创建变量并将其复制以传递给返回的lambda。

\n\n

clang++ -std=c++14 main.cpp && ./a.out

\n\n
\n

Int(int): 0
\n ++n: 0
\n Int(const Int&): 1
\n Int&: 1
\n Int&: 1
\n Int(const Int&): 1
\nn: 1
\n ~Int( ): 1
\n ~Int(): 1
\n ~Int(): 1

\n
\n\n

在这里,它创建变量评估函数,然后传递者复制兰巴。

\n\n

评估顺序为:

\n\n
struct A\n{\n    A(int) { std::cout << "1" << std::endl; }\n    ~A() { std::cout << "-1" << std::endl; }\n};\n\nstruct B\n{\n    B(double) { std::cout << "2" << std::endl; }\n    ~B() { std::cout << "-2" << std::endl; }\n};\n\nvoid f(A, B) { }\n\nint main()\n{\n    f(4, 5.);\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

MSVC 和 GCC:

\n\n
\n

2
\n 1
\n -1
\n -2

\n
\n\n

铛:

\n\n
\n

1
\n 2
\n -2
\n -1

\n
\n\n

正如 clang 中的顺序是向前的一样,lambda 的参数是在函数参数求值之后传递的

\n