文章目录
🦖 右值引用基本概念
右值引用是C++11
后所引入的一个新概念,在C++11
之前(如C++98,C++03)
时也引入了引用的概念,但是该引用大概指的是 左值引用 ,即对左值进行引用;
由于引用的概念可以理解为为一个变量取 别名 ,虽然为取别名,但直接的进行引用并不能为右值进行取别名同时也不能为具有常性的数据取别名;
-
比如
int main() { int &a = 10; char &b = 'c'; double &c = '2.246'; const int num = 10; int& rnum = num; return 0; }
在上述的代码当中所举的例子为使用引用为常量取别名,但是常量实际上也是右值的一种,故在该段代码当中a
,b
,c
,rnum
的引用皆会报错error
;
对于对rnum
的引用而言,具体是因为由于num
变量是具有const
常量的数据,盲目对其进行引用将会造成权限的放大问题故不能被进行引用;
而对于a
,b
,c
三个变量的引用而言不仅是因为其为字面常量,同时也因为其为右值故不能对其进行引用;
当然,普通的引用&
无法对上述的变量进行引用,但若是对上述的变量采用const
引用则可以进行引用;
除此之外,也可以使用 右值引用 对上述变量(除num
以外)进行引用;
右值引用 :'&&' |
如上文所述,右值引用可以对右值进行引用;
-
同上文中的例子相同
int main() { int&& a = 10; char&& b = 'c'; double&& c = '2.246'; const int num = 10; int&& rnum = num; // - error return 0; }
在该段代码中可以观察出由于字面常量属于右值,故使用右值引用可以对常量进行引用,但是不能对具有const
属性的左值进行引用;
🦖 左值与右值的基本概念
在c/C++
的早期中左值与右值在最开始时是根据赋值符号的左右确定的,即赋值符号左侧的数据为左值,而赋值符号右侧的数据为右值;
而在C++11
开始更新 右值引用 语义过后则对左值与右值进行了更加细致的划分;
🪅 什么是 左值(Left Value)
在C++
早期当中,位于赋值符号左侧的变量被称为左值;
而在C++11
更新右值引用过后,左值的概念被更新为:
-
左值(Left Value)
左值的定义不再仅仅与赋值符号有关,左值实际上是指具有标识符的对象,且可以取地址并且可以进行引用,通常左值具有确定的内存位置;
int main() {
// cout << "hello world" << endl;
int a1 = 1, b1 = 2;
a1 = b1;//在C++11之前该种情况下a1为左值b1为右值
//但在C++11后的概念当中 a1与 b1都为左值
//--------
// 以下的p b c *p皆为左值
int *p = new int(0);
int b = 1;
const int c = 2;
//对上面的左值进行引用
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pvalue = *p;
return 0;
}
🪅 什么是 右值(Right Value)
在早期的C++
当中,处于赋值符号右侧的对象被称为右值;
而在C++11
更新右值引用过后,右值的概念被更新为:
-
右值(Right Value)
右值与左值一样也是一种表示数据的表达式;
一般的右值有 字面常量 , 表达式返回值 , 函数返回值 等等;
右值可以出现在赋值符号的右侧但是不能出现在赋值符号的左侧且右值不能通过取地址符号
&
来取到其对应的地址;
int main() {
double x = 1.1, y = 2.2;
//常见的右值
999;
x + y;
fmin(x, y);//包含 <math.h>头文件或<cmath> 该函数返回的是一个临时变量 具有常性
//对右值进行右值引用
int &&rr1 = 999;
double &&rr2 = x + y;
double &&rr3 = fmin(x ,y);
/*
将会出现报错
右值不能放置于赋值符号的左侧
10 = 1;
x + y = 2.2;
fmin(x, y) = 21;
*/
return 0;
}
同时不论是在C++11
标准前后,右值的概念都与是否具有常属性无直接关联;
在早期的C++
标准当中,对右值的定义主要是基于值的生命周期,即右值通常是一个临时对象,它的生命周期通常在表达式结束时结束;
故对于右值来言其不一定一定具备常性;
是否具有常性是右值本身的限制性条件,单纯对于右值而言其可能是常量也可能是非常量;
就和上文所举的例子const int
而言,其具备常属性,但其本身是一个左值数据;
🪅 左值引用与右值引用
-
左值引用
&
左值引用既可以引用左值也可以引用右值;
同时新后用左值时可以直接进行引用,而在引用右值或者是引用带有常性的对象时一般需要加上const以防止权限的放大;
-
右值引用
&&
右值引用可以直接引用右值不需要使用
const
进行修饰;但右值引用不能直接引用左值,若是右值引用直接引用左值的话将会发生
error
(左值包括具有常性的左值 包括const
修饰的左值);但若是需要使用右值引用引用左值的话则需要使用
move()
使得编译器能够在右值引用引用左值时将左值识别为右值(move()
将在下文中进行讲解);
int main() {
//左值引用
int a = 0;
int b = 23;
const int c = 30;
int &ar = a;
int &br = b;
const int &cr = c; //const 引用引用具有常性的左值
const double &dr = 2.22;//const引用引用右值
//右值引用
int &&crr = 30;
char &&charrr = 'c';
cout << b << endl;
// int &&brr = b; 直接引用将会报错
int &&brr = move(b);
cout << b << endl;
return 0;
}
以该段代码为例;
该段代码解释了左值引用与右值引用的区别;
那么存在一个问题:
- 既然引用分为左值引用与右值引用,那么将一个左值引用函数复制一份并将复制的这个函数更改为右值引用,这两个函数是否构成重载?
void Func(int &a) { cout << "左值引用 : void Func(int &a)" << endl; }
void Func(int &&a) { cout << "右值引用 : void Func(int &&a)" << endl; }
int main() {
int a = 10;
int b = 20;
Func(a); // 左值引用 : void Func(int &a)
Func(a + b); //a+b为临时对象 右值引用 : void Func(int &&a)
return 0;
}
以该段代码为例,该段代码当中存在了两个函数,函数名都为Func()
,但唯一不同的是两个函数的参数不同;
根据的重载的定义而言,当两个函数的函数名相同但是其接收参数不同时则两个函数构成重载;
且以不同的参数去调用这个函数,两个参数分别为左值a
与右值a+b
(表达式的返回值)进行调用;
将该段代码运行其结果为:
$ ./mytest
左值引用 : void Func(int &a)
右值引用 : void Func(int &&a)
以该段代码的运行结果来看,实际上左值引用与右值引用可以构成重载;
- 由于函数的接收参数类型不同故构成重载;
🦖 右值(Right Value)的分类
在上文中提到,在C++11
更新了 右值引用(Revalue reference) 后右值的定义被更新为:
- 只能出现在赋值符号的右侧且无法使用取地址
&
符号取到其地址的对象
而在一些教材当中,右值又被分为两种,分别为:
- 纯右值
- 将亡值
🪅 纯右值(Prvalue)
纯右值即为一种临时值,通常纯右值是一些不具名的临时对象或者字面常量;
纯右值一般没有持久性,同时无法取地址;
在表达式当中,通常将纯右值作为右值被使用;
int result = 2+3;
以该行代码为例,其中的2+3
即为纯右值;
🪅 将亡值(Xvalue)
与纯右值不同,将亡值是一种特殊的右值,一般表示一个对象处于 将要过去 的状态;
通常将亡值表示一个值即将被销毁或者移动的值;
将亡值实际上可以被修改,但是其没有持久性,且与纯右值相同,将亡值无法使用取地址符号进行取址;
举一个简单的将亡值的例子:
class Myclass{
public:
Myclass(int _a,int _b):a(_a),b(_b){}
~Myclass(){}
Myclass return_Myclass(Myclass &mc) { return *this; }
private:
int a;
int b;
};
int main() {
Myclass m1(1,2);
Myclass m2 = m1.return_Myclass(m1);
return 0;
}
以该段代码为例,在该段代码当中存在一个名为Myclass
的类;
这个类中存在一个函数名为return_Myclass()
,同时该函数将返回一个对象,且返回值为传值返回;
当出现传值返回时返回过程中将会发生拷贝构造从而生成一个临时拷贝;
那么这里和将亡值有什么关系?
在上面提到将亡值即为一个即将被转移或者是销毁的对象;
而在这段代码当中首先是创建了一个名为m1
的对象,并且m1
去调用其对应的成员函数并返回一个相同类型的对象并用m2
对象进行接收;
这里由于return_Myclass()
函数的返回值是一个传值返回,所以将会生成一个临时的对象,并且返回这个临时对象,而原来的在函数当中的对象在出作用域后将被销毁;
这里即表现为将亡值的将亡属性;
🪅 move() 函数
在上文当中提到了一个函数为move()
函数;
在上文中提到, 右值引用无法直接引用左值,若是需要用右值引用来引用左值的话则需要使用move()
函数使得编译器在使用右值引用引用左值时能够将左值识别为右值;
- 那么
move()
到底是什么;
实际上move()
是一个模板函数,定义在<utility>
头文件当中;
它是C++
标准库中提供的一种用于将左值转换为右值的工具,其作用即为将传入的左值强制转换为 将亡值 ;
-
std::move()
的定义为如下:template <typename T> constexpr std::remove_reference_t<T>&& move(T&& t) noexcept { return static_cast<std::remove_reference_t<T>&&>(t); }
而实际上,并不是将左值对象进行move()
后会将其变为右值,而是move(左值对象)
的这个表达式的返回值将暂时获取 将亡值 的属性;
move()
并不会改变左值对象本身的属性;
🦖 左值引用的使用场景
左值引用与右值引用实际上都是相同的道理,即为一个变量取别名;
int a = 10;
int&b = a;
在此处实际上b
与a
共享同一份资源;
而左值引用实际上的使用场景不仅仅是在为变量取别名;
在类与对象中的拷贝构造当中,可以利用引用传值与传引用返回使得对象能够减少拷贝;
class Myclass {
public:
Myclass(int _value = 10) : data(_value) {
std::cout << data << std::endl;
}
Myclass(const Myclass& other) : data(other.data) {}
Myclass ToAdd(int val) {
data += val;
return *this;
}
int data;
};
int main() {
Myclass m1;
Myclass m2(m1.ToAdd(10));
return 0;
}
以该段代码为例;
该段代码当中存在一个名为Myclass
的类,且该类当中存在一个名为ToAdd()
的成员函数;
但是在该函数当中的返回值为传值返回,这意味着当发生传值返回时将会产生一个临时的拷贝;
而若是将该函数的返回值修改为传引用返回则不会去调用拷贝构造来创建一个新的临时对象;
在这段代码当中将会发生两次拷贝构造函数,分别为在构造m2
时调用拷贝构造一次,在ToAdd()
函数当中由于是传值返回将会调用一次拷贝构造函数;
在这里一些编译器可能会进行优化两次拷贝构造使得其将两次拷贝构造函数优化为一次拷贝构造;
$ ./mytest
Myclass(int _value = 10)
Myclass(const Myclass& other)
Myclass(const Myclass& other)
而若是使用传引用(左值引用)返回则会减少一次拷贝构造;
Myclass& ToAdd(int val) {
data += val;
return *this;
}
将该函数修改为传引用返回并运行该程序;
$ ./mytest
Myclass(int _value = 10)
Myclass(const Myclass& other)
本质上左值引用用于提高效率的方式即为利用引用传值或者是传引用返回从而减少拷贝构造的发生;
但是这种方式既有优点也有缺点;
- 若是当返回值为局部对象时使用传引用返回来返回该对象时由于局部对象在出作用域后被销毁从而导致该引用不再引用有效数据从而造成悬空引用
🦖 右值引用的使用场景
右值引用与左值引用也是同样的道理,其本质上就是为一个对象取别名;
而换个思路来想,左值引用可引用左值,const
左值引用可引用右值;
那么既然如此的话右值引用又有什么用?
- 既然
const
左值引用可引用右值,那么右值引用来引用右值是否会存在语义上的冗余?
实际上右值引用并不是单单的用于引用右值;
在上文中提到,当传值传参或者是传值返回时由于需要生成一个临时的对象所以将要调用对应的拷贝构造函数从而来拷贝一个临时对象;
若类中不存在需要动态开辟内存的情况下可以直接进行浅拷贝,而若是类中存在需要动态开辟的对象时则需要在拷贝构造函数当中再去动态开辟空间并将原有的数据进行释放;
而若是使用穿引用传参或者是传引用返回时,由于引用是对象(变量)的别名,所以当传引用传参或者是传引用返回时将会直接将对象(变量)的别名传入或者返回从而减少拷贝构造的发生来提高整体效率;
但是对应的也存在短板,若是一个对象的生命周期只存在于局部,在此时使用传引用返回时由于对象在出作用域后即会被销毁会导致指针指向一个不存在的对象或者是指向一个已经被销毁的对象导致的 悬空引用 问题,这个问题在某种意义上的危险性堪比于 野指针 ;
在C++11
引入了右值引用过后,右值引用最多的使用场景是资源的转移问题;
该资源转移本质上对应着上文当中所提到的将亡值,也是针对上文当中所提到的悬空引用问题;
当右值引用接收到一个将亡值时,将不再生成临时拷贝,而是直接将资源进行转移,从而降低资源的重复拷贝,且有效利用资源;
#include <iostream>
using namespace std;
int main() {
string s1 = "it's a string";
cout << "s1 : " << s1 << endl;
string s2(move(s1));//move将s1变为将亡值
cout << "-----------------------" << endl;
cout << "s1 : " << s1 << endl;
cout << "s2 : " << s2 << endl;
return 0;
}
以该段代码为例;
存在两个string
变量分别为s1
与s2
,其中在利用s1
构造s2
的过程时使用move()
将s1
变成将亡值;
并再次打印s1
与s2
两个变量;
当运行该程序时,其对应的结果如下:
$ ./mytest
s1 : it's a string
-----------------------
s1 :
s2 : it's a string
当使用move()
函数修饰的s1
对象对s2
进行构造后,可以发现最终s1
对象的资源被转移给了s2
;
s1
最终成为空串;
在上文当中提到move()
的作用是暂时将表达式的返回值判定为右值当中的 将亡值 ;
如下图所示;
🪅 移动构造函数
在C++11
标准当中,引入了一个新的概念 – “移动构造函数”;
移动构造函数 也是构造函数当中的一种,它的引入主要还是为了解决在对象拷贝时导致的性能问题;
在传统的拷贝构造操作中,对象中的所有成员都被逐个复制,对于内置类型来说只进行了浅拷贝所以开销并不成问题;
但若是对于大型对象或者包含动态分配资源的对象来说将会导致昂贵的性能开销;
移动构造函数允许在对象转移时将资源的所有权转移到另一个对象从而不进行不必要的数据复制,从而避免拷贝大型对象或是包含大量动态分配资源的对象时产生的性能开销;
在上文中提到了右值引用的实际场景并举出了一个代码例子;
在运行结果当中发现实际资源进行了移动;
string s1 = "it's a string";
cout << "s1 : " << s1 << endl;
string s2(move(s1));//move将s1变为将亡值
虽然发生了资源的移动但是实际上在代码当中并未出现右值引用&&
的符号;
-
那么这段资源的移动是否与右值引用
&&
有关?是的
在代码当中虽然未曾使用过右值引用符号&&
,但实际上使用了string
容器实例化了对象,而实际上发生资源的移动也是因为string
容器封装了一个 移动构造函数 ;
template<typename _CharT, typename _Traits, typename _Allocator>
class basic_string {
public:
// 移动构造函数
basic_string(basic_string&& __str) noexcept
: _M_dataplus(__str._M_dataplus) {
__str._M_dataplus._M_p = nullptr; // ...
}
private:
struct _Alloc_hider : allocator_type {
_Alloc_hider(pointer __dat, const allocator_type& __a)
: allocator_type(__a), _M_p(__dat) { }
pointer _M_p;
};
typedef typename __gnu_cxx::__alloc_traits<_Alloc>::template
rebind<_CharT>::other _CharT_alloc_type;
struct _Alloc_hider_and_ptr : _Alloc_hider {
_CharT_alloc_type _M_p_allocator;
_Alloc_hider_and_ptr(pointer __dat, const allocator_type& __a)
: _Alloc_hider(__dat, __a), _M_p_allocator(__a) { }
};
_Alloc_hider_and_ptr _M_dataplus;
};
这段代码是一个简化的string
容器的 移动构造函数 ;
可以看到在移动构造函数当中,在初始化列表当中利用传入对象的成员初始化当前对象的对象成员: _M_dataplus(__str._M_dataplus)
;
并在移动构造的函数体内将传入对象的成员置空从而达到资源交换的目的__str._M_dataplus._M_p = nullptr; // ...
;
而从函数的参数类型可以看出实际上发生资源转移时本质上就是去调用了其对应的移动构造函数 basic_string(basic_string&& __str) noexcept
;
-
存在一段代码
#include <iostream> using namespace std; namespace Cherry { struct MyVector { int _len; int _size; int* _arr; // 构造 MyVector(int n = 10) : _len(0), _size(n), _arr(new int[n]) {} //拷贝构造 MyVector(const MyVector& _myv) : _len(_myv._len), _size(_myv._size), _arr(new int[_myv._size]) { for (int i = 0; i < _len; ++i) { _arr[i] = _myv._arr[i]; } cout << "MyVector(const MyVector& _myv) -- 拷贝构造" << endl; } //移动构造 MyVector(MyVector&& myv) : _len(0), _size(0), _arr(nullptr) { swap(myv._arr, _arr); swap(myv._len, _len); swap(myv._size, _size); cout << "MyVector(MyVector&& myv) -- 移动构造" << endl; } //析构函数 ~MyVector() { delete[] _arr; _arr = nullptr; _size = 0; _len = 0; } //插入函数 bool Push_back(int num) { if (_len < _size) { _arr[_len] = num; ++_len; return true; } else { cout << "error" << endl; return false; } } //打印函数 void Print() { for (int i = 0; i < _len; ++i) { cout << _arr[i] << " "; } cout << endl; } }; } // namespace Cherry
在这段代码当中存在了一个名为MyVector
的类,其中类中包括了 构造函数 , 拷贝构造函数 , 移动构造函数 , 析构函数 等成员函数;
其中当若是发生拷贝构造或是移动构造时将会打印出对应的信息;
-
存在一组测试代码:
int main() { Cherry::MyVector mv1; mv1.Push_back(1), mv1.Push_back(3), mv1.Push_back(5), mv1.Push_back(7); Cherry::MyVector mv2(mv1); cout << "mv1打印:" << endl; mv1.Print(); cout << "mv2打印:" << endl; mv2.Print(); Cherry::MyVector mv3(move(mv1)); cout << "mv1打印:" << endl; mv1.Print(); cout << "mv3打印:" << endl; mv3.Print(); return 0; }
若是在不存在 移动构造 的前提当中(即将当前代码当中的移动构造函数注释屏蔽)的情况下运行该段代码;
$ ./mytest
MyVector(const MyVector& _myv) -- 拷贝构造
mv1打印:
1 3 5 7
mv2打印:
1 3 5 7
MyVector(const MyVector& _myv) -- 拷贝构造
mv1打印:
1 3 5 7
mv3打印:
1 3 5 7
从结果当中发现,虽然在测试例子当中的mv3
使用了move()
函数将其暂时变成了将亡值;
但由于不存在移动构造函数故该函数以const MyVector&
类型进行传入并进行拷贝构造,从运行结果可以发现拷贝构造进行了两次;
若是存在移动构造函数的情况下运行该程序:
$ ./mytest
MyVector(const MyVector& _myv) -- 拷贝构造
mv1打印:
1 3 5 7
mv2打印:
1 3 5 7
MyVector(MyVector&& myv) -- 移动构造
mv1打印:
mv3打印:
1 3 5 7
在该结果中可以发现若是在存在移动构造函数的情况下运行该程序时,由于移动构造的参数接收为右值,且在进行传参时mv1
通过move()
函数暂时被判别为右值当中的将亡值从而发生移动构造函数;
🪅 移动赋值函数
右值引用的使用场景不仅仅体现在 移动构造 当中,同样的在默认成员函数当中除了 移动构造函数 还存在着一个 移动赋值;
移动赋值 是C++11
标准当中所引入的一个新特性;
与移动构造相同,其主要目的就是优化对象的资源管理;
与拷贝赋值不同,拷贝赋值着重使用了对象之中的深拷贝,而若是涉及到大量动态分配内存或者大量数据拷贝时,通过移动赋值可以减少不必要的资源拷贝以减少程序的性能;
与移动拷贝相同,赋值时可能会出现两种情况,即普通对象以及即将销毁的对象;
对于普通对象而言可能需要进行深拷贝,而若是面对即将销毁的对象将其复制给另一个对象时,临时进行一次深拷贝并将原有的对象资源进行销毁将会造成浪费;
而移动赋值则有效的将即将销毁的对象资源转移给需要进行复制的对象当中从而有效的提高了效率;
-
我将在上文的代码当中添加三个函数
//多参构造函数 MyVector(initializer_list<int> il) : _len(il.size()), _size(il.size()), _arr(new int[il.size()]) { int* ptr = _arr; for (int val : il) { *ptr++ = val; } } //拷贝赋值 MyVector& operator=(const MyVector& other) { if (this != &other) { // 释放当前对象的资源 delete[] _arr; // 分配新的资源并复制数据 _size = other._size; _len = other._len; _arr = new int[_size]; for (int i = 0; i < _len; ++i) { _arr[i] = other._arr[i]; } } cout << "MyVector& operator=(const MyVector& other) -- 拷贝赋值" << endl; return *this; } //移动赋值 MyVector& operator=(MyVector&& myv) noexcept { if (this != &myv) { // 交换当前对象和源对象的成员变量 swap(_arr, myv._arr); swap(_len, myv._len); swap(_size, myv._size); // 将源对象的成员变量设置为默认值 myv._arr = nullptr; myv._len = 0; myv._size = 0; } cout << "MyVector& operator=(MyVector&& myv) -- 移动赋值" << endl; return *this; }
在这段代码当中增加了三个函数,分别为 多参构造函数 , 移动赋值重载 , 拷贝赋值重载 ;
当触发对应函数调用时将会打印出对应的调用情况;
-
存在一组用例
int main() { Cherry::MyVector mv1 = {1, 2, 3, 4, 5}; Cherry::MyVector mv2 = {2, 2, 2, 2, 2}; mv1 = {2, 4, 6, 8, 9}; mv2.Print(); mv1.Print(); cout << "----------------" << endl; mv1 = move(mv2); mv2.Print(); mv1.Print(); return 0; }
在该段用例当中,mv1
的赋值使用了{2,3,4,8,9}
的方式,即利用了隐式类型转换构造了一个临时对象从而进行赋值;
在第二次赋值时使用了move
函数修饰mv2
使其短时间被转换为右值(将亡值)并赋值给mv1
;
当不存在移动赋值的情况(将移动赋值进行注释)运行该程序;
$ ./mytest
MyVector& operator=(const MyVector& other) -- 拷贝赋值
2 2 2 2 2
2 4 6 8 9
----------------
MyVector& operator=(const MyVector& other) -- 拷贝赋值
2 2 2 2 2
2 2 2 2 2
----------------
从运行结果来看,无论是 将亡值 还是使用了move()
修饰所进行的赋值操作,由于不存在移动构造函数即使触发了移动语义也只进行拷贝赋值;
当存在移动赋值的情况运行该程序;
$ ./mytest
MyVector& operator=(MyVector&& myv) -- 移动赋值
2 2 2 2 2
2 4 6 8 9
----------------
MyVector& operator=(MyVector&& myv) -- 移动赋值
2 2 2 2 2
----------------
从运行结果可以看出,当存在移动赋值时触发了移动语义时将会去调用对应的移动赋值;
且从运行结果当中发现,实际山移动赋值与移动构造的机制相当,即将一个将亡对象的资源转移给另外一个资源从而减少拷贝的发生提高效率;
🪅 move的注意事项
在上文中解释了右值引用的实际使用场景;
同时在上文当中提到了一个函数叫做move()
函数;
move(左值对象)
这个表达式能够将对应的左值转换为 右值(将亡值) 从而能够达到资源转移的目的;
而实际在使用过程当中应该避免使用move()
函数引用左值;
主要是避免了在某些情况资源被转移而导致数据丢失等问题(对象未定义或对象状态被意外更改);
🦖 默认成员函数
在C++11
之前,默认成员函数分别有 构造 , 析构 , 拷贝构造 , 拷贝赋值重载 , 取地址重载 , const
取地址重载 六个默认成员函数;
而在C++11
标准当中又新添加的两个默认成员函数,分别为上文中所出现的 移动构造 , 移动赋值 函数;
既然是默认成员函数,那么将会出现未显示定义时所出现的行为,即未显示定义时将会默认生成的行为;
对于前六个默认成员函数而言,当为显示定义时编译器将会默认生成一个对应的成员函数;
但 移动构造 与 移动赋值 不同,相比之下这两个默认成员函数的生成情况也比其他六个成员函数的条件要更为苛刻;
-
移动构造函数
当未显示定义移动构造函数,且未显示定义析构函数,拷贝构造,拷贝赋值重载中的任意一个,编译器将会自动生成默认的移动构造函数;
对于内置类型而言,默认移动构造将会逐字节进行拷贝;
对于自定义类型而言,默认移动构造将会去调用他们的移动构造;
-
移动赋值
当未显示定义移动赋值重载,且未显示定义析构函数,拷贝构造,拷贝赋值重载中的任意一个,编译器将会自动生成默认的移动赋值;
与移动构造相类似,默认生成的移动赋值对于内置类型而言将会进行浅拷贝;
对于自定义类型而言将会去调用它的移动赋值;
在C++11
标准引用了右值引用的概念以及添加了 移动构造 , 移动赋值 后,对应的STL
当中的插入接口函数也增加了右值引用版本;
🦖 完美转发
在C++11
中引入了一个新的特性叫做 完美转发 ,该特性主要是用来实现 泛型编程 的一种技术,旨在保持参数值类别(左值或右值)并将其传递给其他函数;
在C++11
之前使用模板函数进行参数传递时,对应的所传入的数据将会被识别为左值从而丢失其右值属性从而进行拷贝;
🪅 引用折叠
引用折叠 是C++11
中新引入的概念;
实际上 引用折叠 是参数推导过程中的一个特性,当使用引用类型作为模版参数时,编译器如何处理引用的一种规则;
一般引用折叠的规则如下:
- 当一个对象被引用时,根据引用的左值还是右值,引用的类型可能会被折叠成 左值引用 或是 右值引用;
- 如果一个右值引用(
&&
)和一个非引用类型(T
)进行引用折叠,则折叠结果是右值引用(T&&
); - 如果两个引用类型进行引用折叠,则根据两个引用是否都为右值引用来确定结果:
- 如果两个引用都是右值引用,折叠结果则为右值引用;
- 如果其中一个引用是左值引用而另一个是右值引用,折叠结果即为左值引用;
- 如果两个引用都是左值引用,则折叠结果是左值引用;
具体规则可以通过标准库当中的std::is_rvalue_reference<T>
进行验证;
🪅 万能引用
万能引用 是一种用于描述特殊的模板函数参数形式,这种形式利用了上文当中所出现的引用折叠规则,使得函数能够接受任意类型(这其中包括左值与右值)的参数且保留参数的左值或是右值属性;
万能引用的语法形式通常为T&&
;
template<class T>
myFunc(T&& arg){
;
}
其中T
是模板类型参数,与右值引用看起来类似,但实际上万能引用与右值引用具有实质性的差别;
万能引用 只有在模板类型推导中结合了 右值引用 和 引用折叠 规则时才会发挥作用;
当传递一个 左值 给 万能引用参数 时,模板类型推导会将参数类型推导为 左值引用 ;
当传入一个 右值 给 万能引用参数 时,模板类型推导会将其推导为 右值引用 ;
#include <iostream>
#include <type_traits>
template <class T>
void MyFunc(T&& arg) {
if (std::is_rvalue_reference<T&&>::value) {
std::cout << "右值引用" << std::endl;
} else {
std::cout << "左值引用" << std::endl;
}
}
int main() {
int a = 10;
MyFunc(10);
MyFunc(a);
return 0;
}
利用is_rvalue_reference<T&&>
可以判断其函数内对应的对象是否为右值引用从而判断其中所传入的值是左值引用还是右值引用;
-
运行这个程序最终结果为:
$ ./mytest 右值引用 左值引用
从运行结果可以观察万能引用可以将所传入的值对应的判断为左值引用还是右值引用从而避免了函数重载从而降低代码冗余;
其中万能引用和引用折叠是相辅相成的,引用折叠为万能引用提供支持,而万能引用则利用了引用折叠的规则,使得模板函数能够接受任意类型的参数;
🪅 右值属性丢失
在右值引用传入至函数当中会出现一种现象叫做 右值属性丢失 ;
void funcB(int&& arg) {
std::cout << "funcB: 右值引用" << std::endl;
cout << &arg << endl;
}
void funcB(int& arg) {
std::cout << "funcB: 左值引用" << std::endl;
cout << &arg << endl;
}
void funcA(int&& arg) {
std::cout << "funcA: 右值引用" << std::endl;
cout << &arg << endl;
funcB(arg); // 将参数传递给 funcB
}
void funcA(int& arg) {
std::cout << "funcA: 左值引用" << std::endl;
cout << &arg << endl;
funcB(arg); // 将参数传递给 funcB
}
int main() {
funcA(10); // 调用 funcA,并传入一个右值
return 0;
}
以该段代码为例,该函数中存在两个函数以及其重载,分别为左值引用版本与右值引用版本,同时对应的每个函数中都会根据调用打印对应的信息;
在主函数当中调用funcA()
函数并传入一个常量10
;
在funcA()
函数当中再将该右值引用的数据传入给funcB()
函数;
按照正常来说,右值引用所引用的值为右值,故在funcA()
函数中去调用funcB()
函数时应去调用对应的右值引用版本重载;
-
运行该程序
$ ./mytest funcA: 右值引用 0x7ffe99fff8cc funcB: 左值引用 0x7ffe99fff8cc
从结果来看,实际上在刚开始传入10
至funcA()
函数时所调用的版本是右值引用版本,而在funcA()
函数调用funcB()
函数时调用的是左值引用的重载;
这就是发生了所谓的 右值属性丢失 ;
不仅仅是非模板函数;
-
存在一个程序
#include <iostream> #include <type_traits> template <class T> void MyFunc(T&& arg) { if (std::is_rvalue_reference<T&&>::value) { std::cout << "右值引用" << std::endl; MyFunc(arg); } else { std::cout << "左值引用" << std::endl; } } int main() { MyFunc<int&&>(10); return 0; }
在上文中提到了若是不存在 右值属性丢失 ,该段代码将会进入无穷递归,即调用判断为右值引用过后再次调用MyFunc(arg)
,周而复始;
-
运行该程序
$ ./mytest 右值引用 左值引用
从结果来看,在第一次调用时判断为 右值引用 ;
而在第二次调用时并未发生无穷递归, 第二次的函数调用被判定为 左值引用;
🪅 forward<T&&>()函数
在上文中提到了对于在函数中将右值引用的对象再次利用该对象调用函数时该对象将被判断为左值引用从而丢失其右值属性;
在C++11
版本当中,引入了一个函数std::forward<T&&>()
函数;
template<typename T>
constexpr T&& forward(typename std::remove_reference<T>::type& t) noexcept {
return static_cast<T&&>(t);
}
该函数本质上的功能就是根据传入的参数类型,将参数转发为右值或左值引用从而确保在函数模板当中正确的保持参数的值类别;
void funcB(int&& arg) {
std::cout << "funcB: 右值引用" << std::endl;
cout << &arg << endl;
}
void funcB(int& arg) {
std::cout << "funcB: 左值引用" << std::endl;
cout << &arg << endl;
}
void funcA(int&& arg) {
std::cout << "funcA: 右值引用" << std::endl;
cout << &arg << endl;
funcB(forward<decltype(arg)>(arg)); // 将参数传递给 funcB
}
void funcA(int& arg) {
std::cout << "funcA: 左值引用" << std::endl;
cout << &arg << endl;
funcB(arg); // 将参数传递给 funcB
}
int main() {
// int x = 10;
funcA(10); // 调用 funcA,并传入一个右值
return 0;
}
以该段代码为例;
在该段代码当中的void funcA(int&& arg)
函数当中去调用funcB()
函数;
与上文当中的代码不同,这里的在调用funcB()
函数时使用了forward<T&&>()
函数,其中采用了decltype()
函数来推导其对应的类型从而最终可以正确调用forward<T&&>()
函数;
-
运行该程序
$ ./mytest funcA: 右值引用 0x7ffd3f3ab03c funcB: 右值引用 0x7ffd3f3ab03c
从代码的结果来看,这里通过调用forward<T&&>()
函数从而保持了该右值引用原本的右值属性从而正确调用了funcB()
函数的右值引用重载;
🪅 完美转发
在上文当中提到了forward<T&&>()
函数;
该函数可以在调用过程当中保持参数原本的参数属性,例如左值引用则保持左值引用调用;
右值引用则保持右值引用调用从而避免在调用过程当中出现的右值属性丢失;
但是实际上forward<T&&>()
函数实际上真正的调用场景并不和上文中出现的调用场景类似;
-
在该标题的开始处提到了
“在
C++11
中引入了一个新的特性叫做 完美转发 ,该特性主要是用来实现 泛型编程 的一种技术,旨在保持参数值类别(左值或右值)并将其传递给其他函数;”
而实际上forward<T&&>()
函数真正的场景是用来实现完美转发;
完美转发实际上就是 万能引用 配合 forward<T&&>() 函数所实现的一种灵活的泛型技术;
在模板函数的调用当中,完美转发允许我们将参数按原样转发给另一个函数,同时保持参数的值类别;
万能引用允许参数接收任意类型的参数,并保留参数的值类别(左值或右值),而若是传入下一层函数调用时将会发生值类别的属性丢失问题;
而forward<T&&>()
函数则根据参数的值类别(左值引用或是右值引用)将参数原样转发给其他函数;
这种结合使用的方式可以保持参数的原始类型和值类别,从而实现完美转发的功能;
-
存在一个程序
#include <iostream> #include <utility> // 接受任意类型参数的函数 void process(int& x) { std::cout << "左值引用版本: " << x << std::endl; } void process(int&& x) { std::cout << "右值引用版本: " << x << std::endl; } // 使用完美转发实现参数传递 template<typename T> void forwarder(T&& arg) { process(std::forward<T>(arg)); // 使用std::forward进行完美转发 } int main() { int x = 10; forwarder(x); // 传入左值 forwarder(20); // 传入右值 return 0; }
在该段代码当中存在三个函数,分别为process()
函数(非模板函数)的左值引用及右值引用版本,与一个forwarder()
函数(模板函数);
在主函数当中调用两次forwarder()
且传参分别为左值与右值;
在forwarder()
模板函数当中利用forward()
函数从而实现完美转发;
-
运行该程序
$ ./mytest 左值引用版本: 10 右值引用版本: 20
该程序即为一个简单的完美转发;