C++ 语言特性25 - 表达式求值顺序

一:概述

        在 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++) 的求值顺序未指定,因此不同编译器的结果可能不同。

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

黑不溜秋的

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值