【C++11】三大神器之——右值、移动语义、完美转发-创新互联
如果你还不知道C++11引入的右值、移动语义、完美转发是什么,可以阅读这篇文章;如果你已经对这些知识了如指掌,也可以看看有什么可以补充~😏
创新互联公司2013年成立,是专业互联网技术服务公司,拥有项目成都网站制作、网站建设、外贸网站建设网站策划,项目实施与项目整合能力。我们以让每一个梦想脱颖而出为使命,1280元新乐做网站,已为上家服务,为新乐各地企业和个人服务,联系电话:18982081108一、右值 值类别vs变量类型在正式认识右值之前,我们要先区分值的类别和变量类型:
- 值 (value)和变量 (variable)是两个独立的概念。值不一定拥有变量名(如表达式:i + j + k)。
- 值只有类别(category)之分,而变量只有类型(type)之分。
值类别可以被划分左值和右值。
那什么是左值和右值呢?左值是能被取地址、不能被移动的值。右值是表达式中间结果/函数返回值(可能拥有变量名,也可能没有)。
有一个可以区分左值和右值的便捷方法:看能不能对表达式取地址,如果能,则为左值,否则为右值。
C++11扩展了右值的概念,将右值分为了纯右值和将亡值,但本文不作讨论。
如下的示例将帮助我们区分左值和右值:
int i = 3; // i是左值,3是右值
int j = i+8; // j是左值,i+8是右值
char a = getCh(); // a是左值 ,getCh()的返回值是右值(临时变量)
左值引用、右值引用、常引用在以前的文章中,我们曾经讨论过左值引用和常引用的区别。在本篇文章中,我们需要进一步系统的了解它们三者之间的关系。
引用类型 可以分为两种:
- 左值引用:用
&
符号引用左值(但不能引用右值), - 右值引用:用
&&
符号引用右值(可以移动左值)。
在C++11中,因为增加了右值引用(rvalue reference)的概念,所以C++98中的引用都称为了左值引用(lvalue reference)。
使用方法如下所示:
int&& a = 3; // 3是右值,a是右值引用
int b = 8; // b是左值
int& bb = b; //bb是左值引用
int&& c = b + 5; // b+5是右值,c是右值引用
AA&& aa = getTemp(); // getTemp()的返回值是右值(临时变量)
左值引用十分常见,我们知道是给变量取个别名,但是引入右值引用的意义是什么呢?(将在下文中解答)
在上述的代码中,getTemp()的返回值本来在表达式语句结束后,其生命也就该终结了(因为是临时变量),而通过右值引用重获了新生,其生命周期将与右值引用类型变量aa的生命周期一样,只要aa还活着,该右值临时变量将会一直存活下去。
在下面的代码中将帮助我们区分左值引用和右值引用:
void func(T& a);//1,参数是左值
void func(T&& a);//2,参数是右值
//T类型的变量
T var;
T& rvar1 = var;//正确,rvar1是左值
T& rvar1 = T{};//错误,左值引用不能引用右值
T&& rvar2 = T{};//正确,rvar2是右值
T&& rvar2 = var;//错误,右值引用不能引用左值
T&& rvar2 = std::move(var);//正确,可以通过std::move()将左值转为右值引用
func(var);//进入1,a是左值
func(T{});//进入2,a是右值
func(rvar1);//进入1,a是左值
func(rvar2);//进入1,rvar2是右值引用但a是左值
可以看出:
- 当左值引用变量
rvar1
在初始化时,不能绑定右值T{}
, - 当右值引用变量
rvar2
在初始化时,不能绑定左值var
,但是可以通过std::move()
将左值转为右值引用。 - 在代码的最后,右值引用变量
rvar2
作为实参传入func
中时,在作用域内是左值(已命名的右值引用是左值)。
另外,C++还支持了常引用,能够同时接受左值和右值(作为常引用)。
void func(const T& a);//a是常引用
常引用和右值引用 都能接受右值的绑定,有什么区别呢?
- 常引用可以像右值引用一样将右值的生命期延长,但它有一个缺点是,只能读不能改。
现在回到我们的问题:引入右值引用的意义是什么?
如果函数重载能够同时接受:右值引用/常引用参数,则编译器将优先重载:右值引用参数,即引入右值引用的主要目的是实现移动语义。
下面是不同值作为实参传入形参时,函数重载优先级(数字越小优先级越高):
实参/形参 | T& | const T& | T&& | const T&& |
---|---|---|---|---|
左值 | 1 | 2 | ||
常左值 | 1 | |||
右值 | 3 | 1 | 2 | |
常右值 | 2 | 1 |
在正式学习移动语义(move semantic)和完美转发std::forward()之前,我们还要提一嘴引用折叠(reference collapsing),它是移动语义和完美转发的实现基础。
using Lref = Data&;
using Rref = Data&&;
Data data;
Lref& r1 = data; // r1是左值
Lref&& r2 = data; // r2是左值
Rref& r3 = data; // r3是左值
Rref&& r4 = Data{}; // r4是右值
总之,只有右值引用折叠到右值引用上仍然是一个右值引用,而其他所有的引用类型之间的折叠都将变成左值引用。
二、移动语义 为什么需要移动语义我们知道,如果一个对象中有堆区资源,需要编写拷贝构造函数和赋值函数,实现深拷贝。如果被拷贝的对象是临时的,拷贝完就没什么用了,这样会造成没有意义的资源申请和释放操作。如果能将对象包含的资源,直接从旧对象移动到新对象,就可以节省资源申请和释放的时间。C++11新增加的移动语义(move semantic)就是为了做到这一点(基本类型不包含资源,其移动和拷贝相同。)。
还有另一种情况:如果资源对象本身不可拷贝(如智能指针std::unique_ptr)需要定义移动构造/移动赋值函数,其原理类似。
实现移动语义要增加两个函数:移动构造函数和移动赋值函数。我们通过实现一个简单的string类对象来说明:
class String
{public:
String()
{cout<< "String类"<< this<< "的构造函数"<< endl;// 显示自己被调用的日志
const char* s = "Hello C++";
int len = strlen(s);
_str = new char[len + 1];
strcpy(_str, s);
}
String(const String& another)
{cout<< "String类"<< this<< "的拷贝构造"<< endl;// 显示自己被调用的日志
int len = strlen(another._str);//获取源对象中字符串的长度
_str = new char[len + 1];
strcpy(_str, another._str);// 把数据从源对象中拷贝过来
}
String& operator=(String& another)
{cout<< "String类"<< this<< "的拷贝赋值"<< endl;// 显示自己被调用的日志
if (this == &another)
return *this;// 避免自我赋值
int len = strlen(another._str);//获取源对象中字符串的长度
_str = new char[len + 1];
strcpy(_str, another._str);// 把数据从源对象中拷贝过来
return *this;
}
String(String&& another)noexcept
{cout<< "String类"<< this<< "的移动构造"<< endl;// 显示自己被调用的日志
if(_str != nullptr)
delete[] _str; //如果已分配内存,先释放掉
this->_str = another._str;// 把资源从源对象中转移过来
another._str = nullptr;// 把源对象中的指针置空
}
String& operator=(String&& another)
{cout<< "String类"<< this<< "的移动赋值"<< endl;// 显示自己被调用的日志
if (this == &another)
return *this;// 避免自我赋值
if(_str != nullptr)
delete[] _str; //如果已分配内存,先释放掉
this->_str = another._str;// 把资源从源对象中转移过来
another._str = nullptr;// 把源对象中的指针置空
return *this;
}
friend ostream& operator<<(ostream& out, String& str)
{out<< str._str;
return out;
}
~String()
{cout<< "String类"<< this<< "的析构函数"<< endl;// 显示自己被调用的日志
if(_str != nullptr)
{delete[] _str;
_str = nullptr;
}
}
private:
char* _str;
};
上述代码中,编译器会根据传入的实参的优先级(详见上文表格),来决定重载的构造函数。
- 当实参是左值时,使用拷贝构造,拷贝源对象所有的元素。
- 当实参是右值时,使用移动构造,将指向源对象的内存空间的指针“移动”到新对象,并将源对象的指针置空。
- 拷贝/移动赋值函数的原理相同,在此不再过多描述。
测试用例及输出的结果:
int main()
{{String str1{};//String类0x751f9ffd60的构造函数
cout<< "str1 = "<< str1<< endl;//str1 = Hello C++
String str2{str1};//String类0x751f9ffd58的拷贝构造
cout<< "str2 = "<< str2<< endl;//str2 = Hello C++
//返回一个右值(临时对象)的lambda函数
auto f = [] {String aa; return aa;};
// String str3 = f();//拷贝省略
// cout<< "str3 = "<< str3<< endl;
String str{};//String类0x751f9ffd48的构造函数
String str3{move(str)};//String类0x751f9ffd40的移动构造
cout<< "str3 = "<< str3<< endl;//str3 = Hello C++
String str4{};//String类0x751f9ffd38的构造函数
//String类0x751f9ffd68的构造函数
str4 = f();//String类0x751f9ffd38的移动赋值
//String类0x751f9ffd68的析构函数
cout<< "str4 = "<< str4<< endl;//str4 = Hello C++
}
//String类0x751f9ffd38的析构函数
//String类0x751f9ffd40的析构函数
//String类0x751f9ffd48的析构函数
//String类0x751f9ffd58的析构函数
//String类0x751f9ffd60的析构函数
system("pause");
return 0;
}
尽管C++11引入了移动语义,但是仍有优化的空间——与其调用一次没有意义的移动构造函数,不如让编译器直接跳过这个过程——于是就有了拷贝省略(copy elision)。
移动语义和拷贝省略的区别:
- 移动语义是语言标准提出的概念。是通过编写遵守移动语义的移动构造函数、右值限定成员函数,在逻辑上优化对象内资源的转移流程。
- 拷贝省略是非标准(C++ 17 前)的编译器优化。跳过移动/拷贝构造函数,让编译器直接在移动后的对象内存上,构造被移动的对象。
由于拷贝省略的存在,在上述代码中,String str3 = f();
会被编译器优化,为了方便演示移动构造函数,我们使用了std::move()
的方法移动返回值,当然这会造成不必要的开销。
通用引用阅读本节需要读者有一定的模板编程基础。
C++11中引入了变长模板的概念,允许向模板参数里传入不同类型的不定长引用参数。由于每个类型可能是左值引用或右值引用,针对所有可能的左右值引用组合,特化所有模板 是不现实的。
如果没用通用引用的概念,那么对于一个变长模板函数,至少需要两个重载:
templatevoid func(T& arg, Args&...args)
{func(args...);//左值直接展开
}
templatevoid func(T&& arg, Args&&...args)
{func(std::move(args...));//右值需要std::move()转发
}
Scott Meyers(Effective Modern C++的作者)指出“有时候符号&&
并不一定代表右值引用,它也可能是左值引用。”
事实上,如果一个引用符号需要通过推导才能得出左右值的类型(如模板参数类型或者auto
),那么这个符号就可以是左值引用或右值引用——这就是通用引用 (universal reference)。
这一点我们通过上文中的引用折叠的示例也可以得出。
基于通用引用,我们可以对上述的代码进行改进:
templatevoid func(T&& arg, Args&&...args)
{func(std::forward(args)... );
}
其中std::forward
实现了针对左右值的参数,能保证被转发参数的左、右值属性不变,即完美转发(perfect forwarding)。
在C++11中,完美转发支持:
- 如果模板中(包括类模板和函数模板)函数的参数书写成为
T&& 参数名
,那么,函数既可以接受左值引用,又可以接受右值引用。 - 提供了模板函数
std::forward
,用于转发参数,如果 参数是一个右值,转发之后仍是右值引用;如果参数是一个左值,转发之后仍是左值引用。(参数)
使用示例如下:
void fun1(int& i)//如果参数是左值
{cout<< "左值 = "<< i<< endl;
}
void fun1(int&& i)//如果参数是右值
{cout<< "右值 = "<< i<< endl;
}
templatevoid func(T&& ii)
{fun1(std::forward(ii));
}
完美转发的语法比较简单,至于其原理本文暂时不深入研究。😝
最后本文部分参考自文章
你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧
当前标题:【C++11】三大神器之——右值、移动语义、完美转发-创新互联
分享链接:http://scyanting.com/article/cdessi.html