从上帝视角看。本文主要包含两个方面:
1 省略临时拷贝缘起
从C++标准产生一直到C++17,C++标准一直在试图减少某些临时变量或者拷贝的操作,虽然经过优化后,可能在实际执行中不需要调用拷贝或者移动构造,但是它必须隐士或者显示存在,如下面的案例,如果在类中禁止编译器默认生成拷贝构造和移动构造函数,代码将不会被编译通过。
//代码在C++14版本中编译
class MyClass
{
public:
MyClass(){cout<<"MyClass() initialize..."<<endl;}
MyClass(const MyClass &myClass)=delete;
MyClass(MyClass &&myClass)=delete;
};
MyClass bar() {
return MyClass{};
}
void foo(MyClass param) { // param用 传 递 进 入 的 实 参 初 始 化
cout<<"foo function..."<<endl;
}
int main()
{
foo(MyClass{});
MyClass barClass = bar();
foo(bar());
return 0;
编译后,编译器会报如下错误,该错误产生的原因就是因为在类中限制了拷贝构造和移动构造的默认生成。
从C++17起,上面的代码就可以编译通过了,因为C++17直接强制在临时对象中强制省略了对象的拷贝。但是,C++17还不都彻底,当代码中包含一个具名的变量并作为返回值时依然会调用拷贝构造函数。如下面的代码段:
MyClass bar() {
MyClass myClassObj;
retutn myClassObj;
}
上面的代码在执行时依旧会需要拷贝构造或者移动构造,因为临时对象作为返回值时会触发编译器具名返回值优化。除此之外,以下场景也会调用拷贝或者移动拷贝构造。
void foo(MyClass obj) {
cout<<"foo function..."<<endl;
return obj;
}
上面的代码中,调用拷贝构造或者移动构造是有条件的,如果传进去的形参没有作为函数值返回是不会调用,作为返回值时才会需要,因为返回的对象是具名的。
2 强制省略临时拷贝的优势
强制省略临时拷贝的优势主要有两点:
template<typename T,typename ...Args>
T factory(Args&& ...args){
return T{std::forward<Args>(args)...};
}
int main()
{
int iValue = factory<int>(100);
cout<<"iValue="<<iValue<<endl;
std::unique_ptr<int> pValue =factory<std::unique_ptr<int>>(new int{101});
cout<<"*pValue="<<*pValue<<endl;
std::atomic<int> aValue = factory<std::atomic<int>>(42);
cout<<"aValue="<<aValue<<endl;
return 0;
}
在上面的代码中,泛型函数除了可以用于一般变量的创建外还可以对atomic类型进行创建。在泛型函数中使用了完美转发,具体可以参考下文:
除此之外,在C++17之后类中禁止移动构造函数的默认生成在实际使用时可以正常编译和运行,但是在C++17之前是编译不过的,因为在实际使用时都会调用到移动构造函数。代码如下:
class MyClass
{
public:
MyClass(){cout<<"MyClass() initialize..."<<endl;}
MyClass(int value){cout<<"MyClass(int) initialize..."<<value<<endl;}
MyClass(const MyClass &)=default;
MyClass(MyClass &&)=delete;
};
MyClass bar() {
return MyClass{};
}
int main()
{
MyClass barClass = bar();
MyClass iValue = 4;
return 0;
}
C++17之后,上面的代码就可以正常编译运行,运行结果为:
3 值类型体系 (value category)的变更
东西虽好,但是会伤筋动骨,虽然C++17 明确强制省略了临时拷贝,但是也需要做一系列的配套改动。为了配合改造,C++值类型体系进行了很多改造。
C++值类型体系可以分为三个阶段,分别是:C++11之前的值类型体系,C++11后到C++17期间的值类型体系以及C++17后的值类型体系。具体如下:
3.1 C++11之前的值类型体系
C++11之前。值类型体系主要是从C语言继承而来。划分也比较简单,主要根据赋值语句进行划分,分为左值和右值。如下面的语句:
string strValue="Hello World";
上面语句中,strValue是左值,"Hello World"是右值;当ANSIC出现后,如果在左值前面加上了const限定符。strValue将不能进行赋值,但它依然是一个左值。C++11后因为移动对象又引入了类型到期值,而原来的右值又被称之为纯右值。
3.2 C++11起的值类型体系
自从 C++11 起,值类型有了核心的值类型体系 lvalue(左值), prvalue(纯右值)(”purervalue”) 和 xvalue(到期值)(”eXpiring value”)。而复合的值类型体系有 glvalue(广义左值)(”generalized lvalue”,它是 lvalue 和 xvalue 的复合)和 rvalue(右值)(xvalue 和 prvalue 的复合)
C++11的值类型结构如下:
3.3 C++17起的值类型体系
从C++17起,值类型体系被明确了定义,重新明确后的值类型如下图所示:
从广义来说,值类型主要包含两种形式,分如下:
C++17 引入了实质化 (materialization),这一新的属于主要是针对临时对象。prvalue 就是一种临时对象。因此,临时对象实质化转换实际上就是一种 prvalue 到 xvalue 的转换。
在实际编程时,prvalue 出现在需要 glvalue(lvalue 或者 xvalue)的地方都是有效的,它通过创建一个临时对象prvalue,并用该临时对象完成值的初始化。如下面的代码示例:
void f(const X& p);
f(X());
在上面的代码中,f的参数是一个引用,因此需要一个glvalue的对象。但是x()返回的是一个prvalue,这时。临时变量实质化规则就会呗唤起,将prvalue既X()转换为一个xvalue对象。值得注意的是,这个过程中并没有产生新的对象。
prvalue已经不再是一个对象,而是一个可以进行初始化对象的表达式,因此使用prvalue初始化对象时不需要进行拷贝而是可以进行移动的。这样确保了省略临时对象的拷贝操作可以完美实现。
4 未实质化的返回值传递
以值返回临时对象 (prvalue) 的过程都是在传递未实质化的返回值,主要有以下场景:
int func()
{
return 38;
}
MyClass bar()
{
return MyClass{};
}
decltype(auto) bar()
{
return MyClass{};
}