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

C++ postfix expression undefined vs unspecified behaviour

本文关键字:未指定 未定义 后缀 表达式 C++      更新时间:2023-10-16

提前道歉,我知道评估订单的一般主题已经有很多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"。

我的问题是:

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

  2. 以上是未定义不指定行为的例子吗?为什么(参考标准)?

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

EDIT:我将lambda参数名称从"n"更改为"param",因为这似乎会引起混乱。

具有讽刺意味的是(由于该示例使用C++11功能,其他答案也因此而分心),使该示例具有未指定行为的逻辑可以追溯到C++98第5节第4段

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

本质上,所有C++标准中都存在相同的子句,尽管正如Marc van Leeuwen在评论中所指出的,最近的C++标准不再使用序列点的概念。净效果是一样的:在一个语句中,运算符的操作数和单个表达式的子表达式的顺序或求值保持未指定。

发生未指定的行为是因为表达式n在语句中被求值两次

myLambda(n)(n);

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

类似的子句存在于所有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;
}

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

以上回答了OP的问题1。和2。main()中出现的未指定行为与myLambda()本身的实现方式无关。

为了回答OP的第三个问题,如果lambda(或我的例子中的函子的operator())被修改为不访问其参数的值,则行为仍然是未指定的。唯一的区别是,程序作为一个整体不会产生任何可见的输出,编译器之间的输出可能会有所不同。

lambda定义中的n是lambda定义的函数的形式参数。它与也称为nmyLambda的参数没有关联。这里的行为完全由这两个函数的调用方式决定。在myLambda(n)(n)中,未指定两个函数参数的求值顺序。lambda可以用参数0或1调用,具体取决于编译器。

如果lambda是用[=n]()...定义的,它的行为会有所不同。

我没能找到对标准的正确引用,但我发现它的行为与这里要求的参数评估顺序相似,函数参数评估的顺序没有由标准定义:

5.2.2函数调用

8[注意:后缀表达式和自变量表达式的求值都是相对未排序的。自变量表达式求值的所有副作用都在函数之前排序已输入(参见1.9)。--结束注释]

下面是它在不同编译器的调用中的运行方式:

#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;
}

GCCg++ -std=c++14 -O2 -Wall -pedantic -pthread main.cpp && ./a.out和MSVC

Int(Int):0
Int0
内部(amp;):0
Int(const Int&):0
n:0
~ Int

因此它创建变量并将其复制到返回的lamba。

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

Int(Int):0
++n:0
Int(const Int&):1
Int&:1
内部&:1
Int(const Int&):1
n:1
~ Int

在这里它创建变量evaluates函数,然后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顺序的As是正向的,lambda的参数在函数的参数求值后传递

Ṫ行为是未指定的,因为在函数调用(myLambda(n))(n)中,后缀表达式(我给了它一对多余的括号)相对于自变量表达式n(最右边的一个)是未排序的。但是没有未定义的行为,因为n的修改发生在函数调用myLambda(n)内部。唯一确定的排序关系是,在实际调用lambda之前,评估CCD_ 27和最终的CCD_。对于最后一个问题,如果lambda选择完全忽略其参数,那么行为就不再是未指定的:尽管未指定将什么值作为参数传递,但无论哪种方式都没有可观察到的差异。

子表达式的求值顺序是未指定的,并且可能因运算符&amp;,||,?以及","。

编译器既知道函数原型myLambda,也知道返回lambda(从返回类型中提取)。因为我的第一句话编译器是自由的,他首先评估哪个表达式。这就是为什么永远不应该在表达式中调用具有额外副作用的函数。