VS2017中C++链式cout调用成员函数输出结果不符预期问题
问题复现
你遇到的差异来自两段功能看似一致的C++代码在Visual Studio 2017下的输出不一致:
第一段代码(链式cout输出,VS2017输出01,在线编译器输出00)
#include <iostream> using namespace std; class A { public: A() { a[0] = 1; a[1] = 0; } int a[2]; int b(void) { int x = a[0]; a[0] = a[1]; a[1] = x; return x; } }; int main() { A a; a.b(); cout << a.b() << a.a[1] << endl; return 0; }
第二段代码(拆分cout输出,所有环境均输出00)
#include <iostream> using namespace std; class A { public: A() { a[0] = 1; a[1] = 0; } int a[2]; int b(void) { int x = a[0]; a[0] = a[1]; a[1] = x; return x; } }; int main() { A a; a.b(); cout << a.b(); cout << a.a[1] << endl; return 0; }
根本原因
差异的核心是C++17标准前后对移位运算符<<的操作数求值顺序规定不同,结合b()方法存在修改成员变量的副作用,最终产生了不同输出:
- 首先明确基础逻辑:
b()方法的功能是交换a[0]和a[1]的值,返回交换前的a[0]。对象初始化后第一次调用a.b()执行完成时,数组成员状态固定为a[0]=0、a[1]=1,这一步所有编译器行为一致。 - 链式
cout语句cout << a.b() << a.a[1] << endl;的执行逻辑分两种情况:- 遵循C++17及更新标准的编译器:标准明确规定移位运算符
<<的操作数必须从左到右求值,执行流程固定:- 先调用
a.b():交换数组值为a[0]=1、a[1]=0,返回交换前的a[0]值0 - 再取
a.a[1]的值,此时交换已经完成,值为0 - 按从左到右的顺序输出两个值,结果为
00,和在线编译器、拆分语句的结果一致。
- 先调用
- 遵循C14及更老标准的编译器:标准没有规定
<<操作数的求值顺序,编译器可以自由选择先算左操作数还是右操作数,两种实现都完全符合标准要求。Visual Studio 2017默认使用C14编译模式,对该链式表达式采用从右到左求值、从左到右输出的逻辑:- 先取最右侧的
a.a[1]值,此时还没调用第二次b(),a[1]值为1 - 再调用
a.b():交换数组值为a[0]=1、a[1]=0,返回值0 - 按照
<<的左结合特性,从左到右依次输出之前拿到的两个值:先输出b()的返回值0,再输出提前取到的a[1]值1,最终输出01。
- 先取最右侧的
- 遵循C++17及更新标准的编译器:标准明确规定移位运算符
- 拆分输出语句不存在顺序歧义:两个独立的
cout语句之间存在明确的顺序点,C++所有版本的标准都保证前一个语句的所有副作用(这里指b()对数组的修改)全部执行完成后,才会启动下一个语句的执行,因此无论编译器版本、编译选项如何,执行顺序都固定,输出恒为00。
修复方案
如果需要在VS2017中让第一段代码输出符合预期,有两种可选方式:
- 不依赖编译器的求值顺序规则,将带副作用的函数调用和变量访问拆分到独立语句中,避免在同一个链式表达式里同时出现修改状态的操作和状态读取操作
- 在VS2017项目属性中,将C++语言标准调整为
/std:c++17或更高版本,编译器会遵循新的标准要求按从左到右的顺序求值<<操作数
注意:这类求值顺序未指定的行为不属于程序错误,但跨编译器兼容性极差,生产环境代码不应该依赖特定编译器的未指定实现逻辑,否则更换编译环境、升级编译器版本都可能导致逻辑异常。
内容的提问来源于stack exchange,提问作者Lahiru Dilshan
相关产品推荐
相关产品推荐

