目录
1、引言
2、值类别及相关概念
3、左值、右值
4、左值引用、右值引用
5、移动语义
5.1、为什么需要移动语义
5.2、移动语义定义
5.3、转移构造函数
5.4、转移赋值函数
6、标准库函数 std::move
7、完美转发 std::forward
VC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/124272585C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/125529931C++软件分析工具从入门到精通案例集锦(专栏文章正在更新中...)https://blog.csdn.net/chenlycly/article/details/131405795C/C++基础与进阶(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_11931267.html C++11新特性很重要,作为C++开发人员很有必要去学习,不仅笔试面试时会涉及到,开源代码中会大规模的使用。以很多视频会议及直播软件都在使用的开源WebRTC项目为例,WebRTC代码中大篇幅地使用了C++11及以上的新特性,要读懂其源码,必须要了解这些C++的新特性。所以,接下来一段时间我将结合工作实践,给大家详细讲解一下C++11的新特性,以供借鉴或参考。
C++11引入了对象移动的概念,是一种移动而非拷贝对象的能力,移动对象可以有效地提高程序的性能。
为了支持移动操作,C++11引入了一种新的引用类型 - 右值引用(rvalue references)。所谓右值引用,是必须要绑定到右值的引用,通过&&操作符获得右值引用。今天就来详细讲讲左值、左值引用、右值、右值引用相关的内容。
在C++11中,值类别主要分为左值、左值引用、右值与右值引用。左值引用是绑定到左值的应用,右值引用则是绑定到右值的引用。当右值引用T&&出现在模板函数的参数中,T是模板类型,T&&则是万能引用。万能引用可以接收左值参数,也可以接收右值参数。然后在推导函数参数类型时,可能会发生引用折叠。还会涉及到标准库函数std::move和std::forward。
在C语言中,我们常常会提起左值(lvalue)、右值(rvalue)这样的称呼。一个最为典型的判别方法就是,在赋值表达式中,出现在等号左边的就是“左值”,而在等号右边的,则称为“右值”。如:
int b = 1;
int c = 2;
int a = a + b;
在这个赋值表达式中,a就是一个左值,而b + c则是一个右值。
不过C++中还有一个被广泛认同的说法,那就是可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值。那么这个加法赋值表达式中,&a是允许的操作,但&(b + c)这样的操作则不会通过编译。因此a是一个左值,(b + c)是一个右值。相对于左值,右值表示字面常量、表达式、函数的非引用返回值等。
这里总结一下,什么是左值:
1)能被赋值(充分非必要条件),左值不一定能被赋值,比如const变量,智能在初始化时赋初值,后续不能赋值
2)能取址(充分非必要条件),左值不一定能取址,比如C语言中的register变量:register int i= 3,C++11已经取消了对register的支持,编译时会忽略。再比如C语言中的位域变量是不能取址的:
struct St{ int m:3;} St st; st.m = 3; 指定int型成员m占3个字节。
3)可以初始化左值引用(必要不充分条件),左值可以初始化左值引用,比如:int m = 3; int& n = m;,右值不能初始化左值引用,比如:const int& m = 3; 因为3是右值,不能初始化左值引用,所以编译会报错。
4)字面量属于纯右值,比如1,2,3等立即数就属于字面量,注意,常量字符串“xyz”是左值,可以对该字符串进行取址,即&"xyz"。
5)将亡值可以偷其中的资源。函数的返回值,则是纯右值,返回右值引用,比如std::move函数。
左值引用是对一个左值进行引用的类型,右值引用则是对一个右值进行引用的类型。左值引用和右值引用都是属于引用类型。无论是声明一个左值引用还是右值引用,都必须立即进行初始化。而其原因可以理解为是引用类型本身自己并不拥有所绑定对象的内存,只是该对象的一个别名。
左值引用是具名变量值的别名,而右值引用则是不具名(匿名)变量的别名。
左值引用示例:
int &a = 2; // 左值引用绑定到右值,编译失败, err
int b = 2; // 非常量左值
const int &c = b; // 常量左值引用绑定到非常量左值,编译通过, ok
const int d = 2; // 常量左值
const int &e = c; // 常量左值引用绑定到常量左值,编译通过, ok
const int &b = 2; // 常量左值引用绑定到右值,编程通过, ok
“const 类型 &”为 “万能”的引用类型,它可以接受非常量左值、常量左值、右值对其进行初始化;
右值引用,使用&&表示:
int && r1 = 22;
int x = 5;
int y = 8;
int && r2 = x + y;
T && a = ReturnRvalue();
通常情况下,右值引用是不能够绑定到任何的左值的:
int c;
int && d = c; //err
下面看一个测试示例:
void process_value(int & i) //参数为左值引用
{
cout << "LValue processed: " << i << endl;
}
void process_value(int && i) //参数为右值引用
{
cout << "RValue processed: " << i << endl;
}
int main()
{
int a = 0;
process_value(a); //LValue processed: 0
process_value(1); //RValue processed: 1
return 0;
}
右值引用是用来支持转移语义的。转移语义可以将资源 ( 堆,系统对象等 ) 从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,能够大幅度提高 C++ 应用程序的性能。临时对象的维护 ( 创建和销毁 ) 对性能有严重影响。
转移语义是和拷贝语义相对的,可以类比文件的剪切与拷贝,当我们将文件从一个目录拷贝到另一个目录时,速度比剪切慢很多。通过转移语义,临时对象中的资源能够转移其它的对象里。
在现有的 C++ 机制中,我们可以定义拷贝构造函数和赋值函数。要实现转移语义,需要定义转移构造函数,还可以定义转移赋值操作符。对于右值的拷贝和赋值会调用转移构造函数和转移赋值操作符。
如果转移构造函数和转移拷贝操作符没有定义,那么就遵循现有的机制,拷贝构造函数和赋值操作符会被调用。普通的函数和操作符也可以利用右值引用操作符实现转移语义。
先来看个转移构造函数的实例:
class MyString
{
public:
MyString(const char *tmp = "abc")
{//普通构造函数
len = strlen(tmp); //长度
str = new char[len+1]; //堆区申请空间
strcpy(str, tmp); //拷贝内容
cout << "普通构造函数 str = " << str << endl;
}
MyString(const MyString &tmp)
{//拷贝构造函数
len = tmp.len;
str = new char[len + 1];
strcpy(str, tmp.str);
cout << "拷贝构造函数 tmp.str = " << tmp.str << endl;
}
//移动构造函数
//参数是非const的右值引用
MyString(MyString && t)
{
str = t.str; //拷贝地址,没有重新申请内存
len = t.len;
//原来指针置空
t.str = NULL;
cout << "移动构造函数" << endl;
}
MyString &operator= (const MyString &tmp)
{//赋值运算符重载函数
if(&tmp == this)
{
return *this;
}
//先释放原来的内存
len = 0;
delete []str;
//重新申请内容
len = tmp.len;
str = new char[len + 1];
strcpy(str, tmp.str);
cout << "赋值运算符重载函数 tmp.str = " << tmp.str << endl;
return *this;
}
~MyString()
{//析构函数
cout << "析构函数: ";
if(str != NULL)
{
cout << "已操作delete, str = " << str;
delete []str;
str = NULL;
len = 0;
}
cout << endl;
}
private:
char *str = NULL;
int len = 0;
};
MyString func() //返回普通对象,不是引用
{
MyString obj("mike");
return obj;
}
int main()
{
MyString &&tmp = func(); //右值引用接收
return 0;
}
和拷贝构造函数类似,有几点需要注意:
1)参数(右值)的符号必须是右值引用符号,即“&&”。
2)参数(右值)不可以是常量,因为我们需要修改右值。
3)参数(右值)的资源链接和标记必须修改,否则,右值的析构函数就会释放资源,转移到新对象的资源也就无效了。
有了右值引用和转移语义,我们在设计和实现类时,对于需要动态申请大量资源的类,应该设计转移构造函数和转移赋值函数,以提高应用程序的效率。
直接看转移赋值函数的实例:
class MyString
{
public:
MyString(const char *tmp = "abc")
{//普通构造函数
len = strlen(tmp); //长度
str = new char[len+1]; //堆区申请空间
strcpy(str, tmp); //拷贝内容
cout << "普通构造函数 str = " << str << endl;
}
MyString(const MyString &tmp)
{//拷贝构造函数
len = tmp.len;
str = new char[len + 1];
strcpy(str, tmp.str);
cout << "拷贝构造函数 tmp.str = " << tmp.str << endl;
}
//移动构造函数
//参数是非const的右值引用
MyString(MyString && t)
{
str = t.str; //拷贝地址,没有重新申请内存
len = t.len;
//原来指针置空
t.str = NULL;
cout << "移动构造函数" << endl;
}
MyString &operator= (const MyString &tmp)
{//赋值运算符重载函数
if(&tmp == this)
{
return *this;
}
//先释放原来的内存
len = 0;
delete []str;
//重新申请内容
len = tmp.len;
str = new char[len + 1];
strcpy(str, tmp.str);
cout << "赋值运算符重载函数 tmp.str = " << tmp.str << endl;
return *this;
}
//移动赋值函数
//参数为非const的右值引用
MyString &operator=(MyString &&tmp)
{
if(&tmp == this)
{
return *this;
}
//先释放原来的内存
len = 0;
delete []str;
//无需重新申请堆区空间
len = tmp.len;
str = tmp.str; //地址赋值
tmp.str = NULL;
cout << "移动赋值函数\n";
return *this;
}
~MyString()
{//析构函数
cout << "析构函数: ";
if(str != NULL)
{
cout << "已操作delete, str = " << str;
delete []str;
str = NULL;
len = 0;
}
cout << endl;
}
private:
char *str = NULL;
int len = 0;
};
MyString func() //返回普通对象,不是引用
{
MyString obj("mike");
return obj;
}
int main()
{
MyString tmp("abc"); //实例化一个对象
tmp = func();
return 0;
}
编译器只对右值引用才能调用转移构造函数和转移赋值函数,而所有命名对象都只能是左值引用,如果已知一个命名对象不再被使用而想对它调用转移构造函数和转移赋值函数,也就是把一个左值引用当做右值引用来使用,怎么做呢?标准库提供了函数 std::move,这个函数以非常简单的方式将左值引用转换为右值引用。
int a;
int &&r1 = a; // 编译失败
int &&r2 = std::move(a); // 编译通过
完美转发适用于这样的场景:需要将一组参数原封不动的传递给另一个函数。“原封不动”不仅仅是参数的值不变,在 C++ 中,除了参数值之外,还有一下两组属性:左值/右值和 const/non-const。完美转发就是在参数传递过程中,所有这些属性和参数值都不能改变,同时,而不产生额外的开销,就好像转发者不存在一样。在泛型函数中,这样的需求非常普遍。
下面举例说明:
#include
using namespace std;
template void process_value(T & val)
{
cout << "T &" << endl;
}
template void process_value(const T & val)
{
cout << "const T &" << endl;
}
//函数 forward_value 是一个泛型函数,它将一个参数传递给另一个函数 process_value
template void forward_value(const T& val)
{
process_value(val);
}
template void forward_value(T& val)
{
process_value(val);
}
int main()
{
int a = 0;
const int &b = 1;
//函数 forward_value 为每一个参数必须重载两种类型,T& 和 const T&
forward_value(a); // T&
forward_value(b); // const T &
forward_value(2); // const T&
return 0;
}
对于一个参数就要重载两次,也就是函数重载的次数和参数的个数是一个正比的关系。这个函数的定义次数对于程序员来说,是非常低效的。
那C++11是如何解决完美转发的问题的呢?实际上,C++11是通过引入一条所谓“引用折叠”(reference collapsing)的新语言规则,并结合新的模板推导规则来完成完美转发。
typedef const int T;
typedef T & TR;
TR &v = 1; //在C++11中,一旦出现了这样的表达式,就会发生引用折叠,即将复杂的未知表达式折叠为已知的简单表达式
C++11中的引用折叠规则:
TR的类型定义 |
声明v的类型 |
v的实际类型 |
T & |
TR |
T & |
T & |
TR & |
T & |
T & |
TR && |
T & |
T && |
TR |
T && |
T && |
TR & |
T & |
T && |
TR && |
T && |
注意,一旦定义中出现了左值引用,引用折叠总是优先将其折叠为左值引用。 C++11中,std::forward可以保存参数的左值或右值特性:
#include
using namespace std;
template void process_value(T & val)
{
cout << "T &" << endl;
}
template void process_value(T && val)
{
cout << "T &&" << endl;
}
template void process_value(const T & val)
{
cout << "const T &" << endl;
}
template void process_value(const T && val)
{
cout << "const T &&" << endl;
}
//函数 forward_value 是一个泛型函数,它将一个参数传递给另一个函数 process_value
template void forward_value(T && val) //参数为右值引用
{
process_value( std::forward(val) );//C++11中,std::forward可以保存参数的左值或右值特性
}
int main()
{
int a = 0;
const int &b = 1;
forward_value(a); // T &
forward_value(b); // const T &
forward_value(2); // T &&
forward_value( std::move(b) ); // const T &&
return 0;
}