未排序的值计算(也称为序列点)

26 浏览
0 Comments

未排序的值计算(也称为序列点)

很抱歉再次提及此主题,但是思考这个主题本身已经开始给我带来了未定义的行为。希望能进入明确定义行为的领域。\n给定:\n

int i = 0;
int v[10];
i = ++i;     //Expr1
i = i++;     //Expr2
++ ++i;      //Expr3
i = v[i++];  //Expr4

\n我将以上表达式(按照顺序)理解为:\n

operator=(i, operator++(i));    //Expr1 等价
operator=(i, operator++(i, 0)); //Expr2 等价
operator++(operator++(i));      //Expr3 等价
operator=(i, operator[](operator++(i, 0))); //Expr4 等价

\n现在来讨论行为,以下是关于C++ 0x的重要引用:\n$1.9/12- \"对一个表达式(或子表达式)的求值一般包括值计算(包括为lvalue求值确定对象的身份和为rvalue求值获取以前分配给对象的值)和副作用的初始化。\"\n$1.9/15- \"如果对标量对象的副作用在相对于同一标量对象的另一个副作用或使用相同标量对象值的值计算方面没有顺序,则行为是未定义的。\"\n[ 注意:与不同参数表达式相关的值计算和副作用是无序的。-结尾注释 ]\n$3.9/9- \"算术类型(3.9.1),枚举类型,指针类型,成员指针类型(3.9.2),std::nullptr_t和这些类型的cv限定版本(3.9.3)被统称为标量类型。\"\n- 在Expr1中,对表达式i(第一个参数)的求值与对表达式operator++(i)(具有副作用)的求值是无序的。\n - 因此,Expr1具有未定义的行为。\n- 在Expr2中,对表达式i(第一个参数)的求值与对表达式operator++(i, 0)(具有副作用)的求值是无序的。\n - 因此,Expr2具有未定义的行为。\n- 在Expr3中,需要在外部operator++调用之前完成对唯一参数operator++(i)的求值。\n - 因此,Expr3具有明确定义的行为。\n- 在Expr4中,对表达式i(第一个参数)的求值与对operator[](operator++(i, 0))(具有副作用)的求值是无序的。\n - 因此,Expr4具有未定义的行为。\n这种理解正确吗?

0
0 Comments

在考虑到上述提到的表达式时,我发现想象一台内存具有互锁功能的机器很有用。这样,作为读取-修改-写入序列的一部分,读取内存位置将导致除序列的最后一次写入之外的任何尝试读取或写入都被暂停,直到序列完成。这样的机器并不是一个荒谬的概念;事实上,这样的设计可以简化许多多线程代码的情况。另一方面,如果'x'和'y'是对同一变量的引用,并且编译器生成的代码类似于读取和锁定 reg1=y; reg2=reg1+1; 写入 x=reg1; 写入并解锁 y=reg2,那么像 "x=y++;"这样的表达式在这样的机器上可能会失败。如果y与同一变量别名,则写入x将导致处理器被锁定。

这个问题的主要原因是,在上述机器的设计中,内存读取和写入操作会被暂停,直到整个读取-修改-写入序列完成。这可能导致在某些情况下出现意外的行为,特别是当涉及到对同一变量的多个引用时。在上述例子中,如果x和y引用同一变量,并且编译器生成的代码按照顺序执行,那么写入x的操作将导致处理器被锁定,因为y也被锁定了。

为了解决这个问题,可以采取一些方法。一种方法是在编译器层面进行优化,以确保生成的代码不会导致上述问题。例如,在上述例子中,编译器可以生成一个不会导致处理器锁定的代码序列,例如先将y的值存储在一个临时变量中,然后再执行y的增加操作,并将结果存储在x中。这样,处理器不会被锁定,代码的执行顺序也不会受到影响。

另一种方法是在程序员层面进行编程时避免出现这样的问题。程序员可以选择使用其他方式来表达他们的意图,而不是依赖于可能导致问题的代码。例如,在上述例子中,程序员可以分别执行y的增加操作和将y的值存储在x中的操作,以避免可能导致处理器锁定的情况。

总之,Unsequenced value computations(即序列点)的问题主要是由于内存读取和写入操作被暂停,导致可能出现意外行为。为了解决这个问题,可以在编译器层面进行优化,确保生成的代码不会导致问题,或者在编程时避免出现可能导致问题的代码。

0
0 Comments

(Unsequenced value computations (a.k.a sequence points))这个问题的出现的原因是因为C++中的表达式求值顺序问题。C++中的表达式求值顺序问题是指在一个表达式中,各个子表达式的求值顺序是不确定的,这可能导致未定义行为。下面是关于这个问题的一些讨论:

在C++中,原生的操作符表达式和重载的操作符表达式是不同的。对于原生类型,值绑定到函数参数的过程中存在一个序列点(sequence point),这使得operator++()的版本具有良好定义。但是对于原生类型的情况,这个序列点是不存在的。

根据C++标准,一个标量对象在两个序列点之间的求值过程中,它的存储值最多只能被修改一次。

在C++0x中,对操作数的值计算在操作符的结果值计算之前进行。如果一个标量对象的副作用在与同一标量对象的另一个副作用或使用同一标量对象的值计算之间没有顺序,那么行为是未定义的。

注意,值计算和副作用是两个不同的概念。如果++i等价于i = i+1,那么+是值计算,=是副作用。

尽管在C++0x中,值计算的顺序比C++03更强,但副作用的顺序并不是。

根据C++标准,值计算按照它们的数据依赖性进行排序,除了副作用外,它们的计算顺序是不可观察的。

在C++中,赋值操作在右操作数和左操作数的值计算之后,赋值表达式的值计算之前进行。

根据标准,++ ++x等价于(x +=1) +=1。所以,我们可以这样解释这个表达式:

1. 首先计算最右边的1,并进入括号内。

2. 计算内部的1以及x的值(prvalue和glvalue)。

3. 现在我们需要+=子表达式的值。

a. 我们已经完成了该子表达式的值计算。

b. 赋值的副作用必须在赋值的值可用之前进行排序!

4. 将新值赋给x,这与子表达式的glvalue和prvalue结果是相同的。

5. 现在我们已经解决了这个问题。整个表达式现在被简化为x +=1。

所以,1和3是良好定义的,2和4是未定义行为,这是可以预期的。

从上述讨论可以看出,C++中的表达式求值顺序问题是一个复杂的问题,容易导致未定义行为。为了避免这种问题,编程时应尽量避免复杂的表达式,尽量将表达式拆分为多个简单的语句,以确保求值顺序是明确定义的。此外,在编写重载操作符时,应注意确保其行为与原生操作符一致,以避免出现不一致的情况。

总结起来,C++中的表达式求值顺序问题是一个复杂的问题,容易导致未定义行为。在编程时,应尽量避免复杂的表达式,拆分为多个简单的语句,以确保求值顺序是明确定义的。在编写重载操作符时,应注意确保其行为与原生操作符一致。

0