许多代码库和c++书籍包含的代码直观的看起来貌似是有效的,但是严格地说,它们具有未定义的行为。一个例子是在一个字符串中查找和替换多个子字符串:
例1:
#include <iostream>
#include <string>
int main(void)
{
std::string s = "I heard it even works if you don't believe";
s.replace(0, 8, "").replace(s.find("even"), 4, "sometimes")
.replace(s.find("you don't"), 9, "I");
std::cout << s << std::endl;
return 0;
}
通常的假设是,这段代码是有效的,用“”替换前8个字符,“sometimes”替换“even”,“I”替换“you don't”,因此我们认为得到的结果是:
it sometimes works if I believe
然而,在C++17之前,这个输出结果是不保证的,因为find()调用返回从何处开始替换,可以在处理整个语句时的任何时候执行,并且在需要它们的结果之前完成执行。事实上,所有find()调用(计算替换的起始索引)都可能在任何替换发生之前进行处理,因此,最终字符串的结果可能有很多可能:
在https://ideone.com/上运行结果为:it sometimes works if I believe
在win7 X64,visual studio 2019(c++14)上运行结果为:it even worsometimesf youIlieve
在ubuntu18.04 X64上运行结果为:it sometimes works if I believe
在https://www.onlinegdb.com上运行结果为:it even worsometimesf youIlieve
在https://wandbox.org上运行结果为:it sometimes works if I believe
另一个例子是,考虑使用output运算符打印由相互依赖的表达式计算的值:
例2:
#include <iostream>
int g()
{
return 111;
}
int f()
{
return 222;
}
int h()
{
return 333;
}
int main(void)
{
std::cout << f() << g() << h();
return 0;
}
通常的假设是f()在g()之前被调用,两者都在h()之前被调用。然而,这种假设是错误的。f()、g()和h()可以以任何顺序调用,当这些调用相互依赖时,可能会产生令人不解的结果。
在https://ideone.com/上运行结果为:222111333
在win7 X64,visual studio 2019(c++14)上运行结果为:222111333
在ubuntu18.04 X64上运行结果为:222111333
在https://www.onlinegdb.com上运行结果为:222111333
在https://wandbox.org上运行结果为:222111333
作为一个具体的例子,直到c++ 17以下代码都有未定义的行为:
例3:
#include <iostream>
int main()
{
int i = 0;
std::cout << ++i << ' ' << --i << '\n';
return 0;
}
c++17之前测试结果如下:
在win7 X64,visual studio 2019(c++14)上运行结果为:0 0
在ubuntu18.04 X64上运行结果为:1 0
为了解决所有这些意想不到的行为,对一些操作符来说进行了改进,现在它们指定了一个有保证的结果顺序:
- 对于
e1 [ e2 ]
e1 . e2
e1 .* e2
e1 ->* e2
e1 << e2
e1 >> e2
e1保证在e2之前被求值,所以求值顺序是从左到右。
但是,请注意,相同函数调用的不同参数的计算顺序仍然是未定义的。也就是说,对于如下:
e1.f(a1,a2,a3)
e1肯定会在a1 a2 a3之前求值。然而,a1、a2、a3的求值顺序仍未确定。
- 在所有赋值运算符中
e2 = e1
e2 += e1
e2 *= e1
右边e1保证在左边e2之前求值。
例如,
#include <iostream>
#include<map>
int main()
{
std::map<int, int> tmp;
//对于std::map的[]运算符重载函数,在使用[]新增key时,std::map就已经插入了一个新的键值对
tmp[0] = tmp.size();//C++17保证此处插入的是{0, 0}
std::cout << "value:" << tmp[0] << std::endl;
return 0;
}
- 最后,在new表达中,比如
new Type(e)
现在保证内存分配是在计算e之前执行,并且保证新值的初始化是在使用分配的内存和使用初始化的值之前进行,也就是说保证在使用分配的内存之前用新的值已经初始化了。
因此,从c++ 17起
std::string s = "I heard it even works if you don't believe";
s.replace(0,8,"").replace(s.find("even"),4,"always")
.replace(s.find("don't believe"),13,"use C++17");
保证将s的值更改为:
it always works if you use C++17
因此,在计算find()表达式之前,find()表达式前面的每个替换都已经完成。
另一个结果是
i = 0;
std::cout << ++i << ' ' << --i << '\n';
对于支持这些操作数的任何类型的i,现在保证输出为1 0。
然而,大多数其他操作符的未定义顺序仍然存在。例如:
i = i++ + i; // still undefined behavior
在这里,右边的i可能是i在增加之前或之后的值。
新表达式求值顺序的另一个应用程序是在传递参数之前插入空格的函数[后续在处理空参数包的时候会用到]。