一:概述
在 C++ 中,表达式求值顺序(Order of Evaluation)是一个非常复杂的概念,它决定了在表达式中各个子表达式的求值顺序。
在 C++17 之前,C++ 对于表达式的求值顺序有许多未指定和未定义的行为。编译器在求值顺序上有较大的自由度,这可能导致在复杂表达式中出现不可预测的结果。尤其是在涉及副作用(如递增、递减运算)时,求值顺序的不确定性可能会引发未定义行为。
在 C++17 中,表达式求值顺序得到了进一步的改善和明确。C++17 引入了一些新的规则,减少了许多与未定义行为相关的问题,特别是针对函数调用参数和子表达式的求值顺序。
二:C++ 中的顺序点介绍
在 C++17 之前,C++ 中有“顺序点”(sequence point)的概念,表示某些操作之间有明确的顺序,保证在这些顺序点之前的操作一定会完成。顺序点(Sequence Points) 是 C++ 标准中的一个概念,表示程序中某些操作必须在某些特定的时间点之前或之后完成。在顺序点之前的所有副作用(如变量的修改)都必须完成,而顺序点之后的操作都要从一个新的状态开始。
C++11 及其之前的版本(包括 C++03)明确规定了一些常见的顺序点,而在 C++11 之后,"顺序点" 概念被 "序列化前"(sequenced-before)和 "序列化后"(sequenced-after)等更精确的术语所取代。不过,顺序点仍然是理解早期版本 C++ 中行为的关键。以下是 C++11 之前的一些常见顺序点:
1. 完整表达式结束时的顺序点
在每个完整的表达式(即语句末尾的分号 ;)都是一个顺序点。在顺序点之前的所有操作都必须完成,之后的操作从一个新的状态开始。
int x = 0;
x = 5; // 完整表达式,这是一个顺序点
x = x + 1;
//在这段代码中,x = 5; 是一个完整表达式,顺序点保证在它执行完之后,x 已经被赋值为 5。在下一个表达式 x = x + 1; 执行时,x 的值已经是 5,所以结果是 6。
2. 逻辑与 && 和逻辑或 || 运算符的短路求值
逻辑与 && 和逻辑或 || 运算符在其左操作数求值后会产生一个顺序点。如果左操作数的值足以确定整个表达式的值,右操作数不会被求值,称为短路求值。
bool f1() {
std::cout << "f1 called\n";
return false;
}
bool f2() {
std::cout << "f2 called\n";
return true;
}
int main() {
bool result = f1() && f2(); // f2() 不会被调用,因为 f1() 返回 false
return 0;
}
//在这段代码中,f1() 返回 false,由于逻辑与 && 的左侧已经确定整个表达式的值为 false,所以 f2() 不会被调用。这就是短路求值,并且 f1() 的求值和返回 false 是一个顺序点
3. 逗号运算符 ,
逗号运算符有一个顺序点,保证左边的表达式在右边的表达式求值之前完成。
int x = 0;
x = (x + 1, x + 2);
//在这个例子中,x + 1 先被计算,但是结果会被丢弃。然后 x + 2 被计算,最终结果赋给 x。逗号运算符确保左侧的 x + 1 在 x + 2 之前完成
4. 条件运算符 ?:
条件运算符 ?: 的第一个操作数和被选择的第二或第三个操作数之间存在顺序点。首先计算条件表达式,然后根据结果选择第二或第三个操作数进行求值
int x = 1;
int y = (x > 0) ? x + 1 : x - 1;
//在这个例子中,首先会对 x > 0 进行求值,确定条件为 true,然后只会计算 x + 1,而不会计算 x - 1。条件判断和 x + 1 之间有一个顺序点
5. for 循环的每次迭代之间
在 for 循环的每次迭代之间存在一个顺序点,保证当前迭代中的所有操作在下一次迭代开始之前完成。
for (int i = 0; i < 10; ++i) {
std::cout << i << std::endl;
}
//在这段代码中,每次输出 i 的操作和自增操作 ++i 之间存在顺序点。即在当前迭代的 std::cout 操作和下一次 ++i 之间有一个顺序点。
6. 函数调用
函数调用是一个顺序点,确保所有参数的副作用在函数调用之前完成,且函数调用的副作用在函数返回之前完成。
void foo(int x) {
std::cout << x << std::endl;
}
int main() {
int y = 1;
foo(y++); // 在调用 foo 之前,y++ 的副作用已经完成
}
//在调用 foo(y++) 之前,y++ 的副作用(将 y 增加到 2)已经完成,并且 foo 会接收 1 作为参数。
三:C++17 中表达式求值顺序的变化和改进
1. 函数参数的求值顺序
在 C++17 之前,函数参数的求值顺序是未指定的,意味着不同编译器可以采用不同的顺序,这可能导致不可预测的行为。在 C++17 中,函数参数的求值顺序从左到右被明确规定。
int f(int a, int b) {
return a + b;
}
int i = 1;
f(i++, i++); // C++17 之前的行为未定义
/*
在 C++17 中:
i++ 将按照从左到右的顺序进行求值。
第一个 i++ 将使用当前值 1,然后 i 增加到 2。
第二个 i++ 使用当前值 2,然后 i 增加到 3。
因此 f(i++, i++) 的结果将是 3。
*/
2. 赋值运算符的求值顺序
在 C++17 中,赋值运算符的左侧操作数(LHS)将在右侧操作数(RHS)之前进行求值。这意味着左侧的对象会先被确定,然后再计算右侧的值。(备注: LHS 即 Left Hand Side)
int x = 1;
x = (x = 5) + 1;
/*
在这个例子中:
右侧 (x = 5) 会首先执行,将 x 的值设置为 5,并返回 5。
然后计算 5 + 1,并将结果 6 赋给 x。
*/
3. 逻辑运算符的短路求值
C++17 中的逻辑运算符 &&(逻辑与)和 ||(逻辑或)依然遵循短路求值规则,这意味着:
- 对于
&&,如果左操作数为false,右操作数不会被求值,因为结果已经确定为false。 - 对于
||,如果左操作数为true,右操作数不会被求值,因为结果已经确定为true。
bool f() {
std::cout << "f() called" << std::endl;
return true;
}
bool result = false && f(); // f() 不会被调用
//由于 false && f() 中的左侧为 false,右侧的 f() 不会被调用
4. 逗号运算符的求值顺序
C++17 保留了逗号运算符的从左到右求值顺序。逗号运算符分隔的表达式会按照从左到右的顺序依次求值,最后一个表达式的结果作为整个表达式的结果。
int a = (1, 2, 3); // a 最终为 3
//在这个例子中,1, 2, 3 依次求值,但最终结果是 3,并赋值给 a。
5. 条件运算符的求值顺序
条件运算符 ?: 的求值顺序在 C++17 中也有明确规定:
- 首先对条件表达式进行求值。
- 然后根据条件表达式的结果,选择性地求值第二个或第三个操作数。
int x = 5;
int y = (x > 0) ? 1 : -1;
/*
在这个例子中:
首先求值 x > 0。
由于 x > 0 为 true,1 被求值并赋给 y。
*/
6. 临时对象销毁顺序
在 C++17 中,临时对象的销毁顺序遵循构造顺序的逆序。也就是说,临时对象的构造顺序与其销毁顺序相反。
struct A {
A() { std::cout << "A constructed\n"; }
~A() { std::cout << "A destructed\n"; }
};
struct B {
B() { std::cout << "B constructed\n"; }
~B() { std::cout << "B destructed\n"; }
};
int main() {
A a;
B b;
}
/*
在这个例子中:
A 对象先被构造,B 对象后被构造。
程序结束时,B 先析构,A 后析构,符合逆序销毁规则。
*/
7. 强制序列化 (Sequenced Before)
C++17 引入了强制序列化的概念来进一步明确表达式求值顺序。强制序列化的规则如下:
- 如果一个表达式中的某部分必须在另一部分之前完成,那么称为**“顺序化”**。
- 如果两个操作没有明确的顺序关系,则它们是并发执行的。
int i = 0;
i = ++i + i++;
//在 C++17 中,这种表达式仍然是未定义行为,因为同时修改和读取 i,且没有明确的顺序点。因此,不同编译器可能会产生不同的结果。
8. 未指定的求值顺序
即使在 C++17 中,某些操作仍然是未指定的,比如运算符的操作数之间的求值顺序。对于加法、乘法等运算符,它们的操作数的求值顺序仍然可能因编译器不同而变化。因此,在涉及副作用的情况下,应该避免依赖求值顺序。
int x = 1;
int y = (x++) + (x++); // 仍然是未定义行为
//在这个例子中,由于 (x++) 的求值顺序未指定,因此不同编译器的结果可能不同。
3393

被折叠的 条评论
为什么被折叠?



