[请回答C++] C++11&&auto&&右值引用&&移动语义&&完美转发
C++11提供了很多的新特性,但是这些新特性不是所有都在编程得时候能够发挥出提高效率的特性,因此有些特性值得吸取,有些特性还是沿用98比较好
列表初始化
C++11扩大了用大括号括起来的列表{初始化列表}
的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号,也可不添加。
struct Point
{
int _x;
int _y;
};
//使用大括号对内置类型进行初始化
int x1 = { 1 }; //可添加等号
int x2{ 2 }; //可不添加等号
//使用大括号对数组元素进行初始化
int array1[]{1, 2, 3, 4, 5}; //可不添加等号
int array2[5]{0}; //可不添加等号
//使用大括号对结构体元素进行初始化
Point p{ 1, 2 }; //可不添加等号
//C++11中列表初始化也可以用于new表达式中(C++98无法初始化)
int* p1 = new int[4]{0}; //不可添加等号
int* p2 = new int[4]{1,2,3,4}; //不可添加等号
return 0;
自定义类型的列表初始化
initializer_list
该容器
- 提供了begin和end函数,用于支持迭代器遍历。
- 以及size函数支持获取容器中的元素个数。
多个对象想要支持列表初始化,需给该类(模板类)添加一个带有initializer_list类型参数的构造函数即可。
有两种使用方法
auto il1 = { 10, 20, 30 };
std::initializer_list<int> il2 = { 1, 2, 3, 4 };
initializer_list本质就是一个大括号括起来的列表,如果用auto关键字定义一个变量来接收一个大括号括起来的列表,然后以typeid(变量名).name()
的方式查看该变量的类型,此时会发现该变量的类型就是initializer_list。
initializer_list的使用场景
C++98并不支持直接用列表对容器进行初始化,这种初始化方式是在C++11引入initializer_list后才支持的。
而这些容器之所以支持使用列表进行初始化,根本原因是因为C++11给这些容器都增加了一个构造函数,这个构造函数就是以initializer_list作为参数的。
这样的话不用再一个个push_back初始化了
vector<int> v = { 1, 2, 3, 4, 5 };
list<int> lt{ 10, 20, 30 };
vector<Date> vd = { { 2022, 1, 17 }, Date{ 2022, 1, 17 }, { 2022, 1, 17 } };
map<string, int> dict = { make_pair("sort", 1), { "insert", 2 } };
注意圆括号和花括号之间初始化的区别,圆括号的话,会被识别为一个逗号表达式,所以要注意区分
他这样的语法能够支持的原因就是因为构造的时候支持了initializer_list
vector构造函数
map构造函数
initializer_list实现类似这种构造方式的原理
也就是先控制好size,然后利用迭代器先去遍历获取数据,一个个通过insert或者是push_back来注入容器
namespace allen
{
template<class T>
class vector {
public:
typedef T* iterator;
vector(initializer_list<T> l)
{
_start = new T[l.size()];
_finish = _start + l.size();
_endofstorage = _start + l.size();
iterator vit = _start;
/*typename initializer_list<T>::iterator lit = l.begin();
while (lit != l.end())
{
*vit++ = *lit++;
}*/
for (auto e : l)
*vit++ = e;
}
vector<T>& operator=(initializer_list<T> l) {
vector<T> tmp(l);
std::swap(_start, tmp._start);
std::swap(_finish, tmp._finish);
std::swap(_endofstorage, tmp._endofstorage);
return *this;
}
private:
iterator _start;
iterator _finish;
iterator _endofstorage;
};
}
说明一下:
🐦 在构造函数中遍历initializer_list时可以使用迭代器遍历,也可以使用范围for遍历,因为范围for底层实际采用的就是迭代器方式遍历。
🐦 使用迭代器方式遍历时,需要在迭代器类型前面加上typename关键字,指明这是一个类型名字。因为这个迭代器类型定义在一个类模板中,在该类模板未被实例化之前编译器是无法识别这个类型的。
🐦 最好也增加一个以initializer_list作为参数的赋值运算符重载函数,以支持直接用列表对容器对象进行赋值,但实际也可以不增加。
变量类型推导
auto
在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。
C++11中废弃auto原来的用法,将其用于实现自动类型推断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。
int main()
{
int i = 10;
auto p = &i;
auto pf = strcpy;
cout << typeid(p).name() << endl; //int *
cout << typeid(pf).name() << endl; //char * (__cdecl*)(char *,char const *)
map<string, string> dict = { { "sort", "排序" }, { "insert", "插入" } };
//map<string, string>::iterator it = dict.begin();
auto it = dict.begin(); //简化代码
return 0;
}
除了简化代码,auto 还避免了对类型的“硬编码”,也就是说变量类型不是“写死”的,而是能够“自动”适应表达式的类型。
auto使用
auto 总是推导出“值类型”,绝不会是“引用”;
auto 可以附加上 const、volatile、*、& 这样的类型修饰符,得到新的类型。
decltype
auto使用的前提是:必须要对auto声明的类型进行初始化,否则编译器无法推导出auto的实际类型。但有时候可能需要根据表达式运行完成之后结果的类型进行推导,因为编译期间,代码不会运行,此时auto也就无能为力。
decltype 的形式很像函数,后面的圆括号里就是可用于计算类型的表达式(和 sizeof 有点类似)
其他方面就和 auto 一样了,也能加上 const、*、& 来修饰。
int i = 10;
auto p = &i;
auto pf = strcpy;
cout << typeid(p).name() << endl;
cout << typeid(pf).name() << endl;
decltype(pf) px;
cout << typeid(px).name() << endl;
decltype 不仅能够推导出值类型,还能够推导出引用类型,也就是表达式的“原始类型”。
decltype在这里无非就是可以之创建一个类型而不赋值初始化
const int x = 1;
double y = 2.2;
decltype(x * y) ret; // ret的类型是double
//auto ret = x*y;
decltype(&x) p; // p的类型是int*
cout << typeid(ret).name() << endl;
cout << typeid(p).name() << endl;
decltype是根据表达式的实际类型推演出定义变量时所用的类型,比如:
- 推演表达式类型作为变量的定义类型
- 推演函数返回值的类型
定义类
在定义类的时候,因为 auto 被禁用了,所以这也是 decltype 可以“显身手”的地方。
class DemoClass final
{
public:
using set_type = std::set<int>; //集合类型别名
private:
set_type m_set;//使用别名定义成员变量
//使用decltype计算表达式的类型,定义别名
using iter_type = decltype(m_set.begin());
iter_type m_pos;//类型别名定义成员变量
};
STL新容器和新操作
C++11中新增了四个容器,分别是array、forward_list、unordered_map和unordered_set。
array
一个静态的数组容器,就是一个用容器的反式来定义数组,array的位置在栈上
array和数组
唯一array和数组很大的区别就是array的检查越界是不一样的,数组的越界检查时通过标志位的,然而array会抛异常
如果要用一个词来评价array,就是鸡肋,如杨修说鸡肋,食之无味,弃之可惜
forword_list
forward_list的本质是单链表,缺陷是只支持insert_after和erase_after,因为效率原因如果要支持普通的insert(前插)和erase(删除),必须遍历才能找到删除的前一个和插入的前一个位置的指针连接
插入对比
删除对比
如果要用一个词来评价forward_list,就是鸡肋,如杨修说鸡肋,食之无味,弃之可惜
forward_list很少使用,原因如下:
forward_list只支持头插头删,不支持尾插尾删,因为单链表在进行尾插尾删时需要先找尾,时间复杂度为O(N)。
forward_list提供的插入函数叫做insert_after,也就是在指定元素的后面插入一个元素,而不像其他容器是在指定元素的前面插入一个元素,因为单链表如果要在指定元素的前面插入元素,还要遍历链表找到该元素的前一个元素,时间复杂度为O(N)。
forward_list提供的删除函数叫做erase_after,也就是删除指定元素后面的一个元素,因为单链表如果要删除指定元素,还需要还要遍历链表找到指定元素的前一个元素,时间复杂度为O(N)。
push_back
支持右值插入
emplace
支持右值emplace,这个后面右值提到
主要为了提高效率
字符串转换函数
C++11提供了各种内置类型与string之间相互转换的函数,比如to_string、stoi、stol、stod等函数。
内置类型转换为string
将内置类型转换成string类型统一调用to_string函数,因为to_string函数为各种内置类型重载了对应的处理函数。
string转换成内置类型
如果要将string类型转换成内置类型,则调用对应的转换函数即可。
右值引用和移动语义
Intro of rvalue reference
C++98中提出了引用的概念,引用即别名,引用变量与其引用实体公用同一块内存空间,而引用的底层是通过指针来实现的,因此使用引用,可以提高程序的可读性。
为了提高程序运行效率,C++11中引入了右值引用,右值引用也是别名,但其只能对右值引用。
左值与右值
左值
左值与右值是C语言中的概念,但C标准并没有给出严格的区分方式
一般认为:可以放在=左边的,或者能够取地址的称为左值
只能放在=右边的,或者不能取地址的称为右值,但是也不一定完全正确。
所以一般认为左值是一个标识数据的表达式(变量名或者是解引用的指针),一般情况下,我们可以取它地址+可以对它赋值,左值可以出现在赋值符号的左边,但是特殊情况是const修饰之后的左值,不能给他赋值,但是可以取它的地址,左值引用就是给左值的引用,给左值取别名
//以下的p,b,c,*p都是左值
int * p = new int(0);
int b = 1;
const int c = 2;
总结一下就是:对于左值来说
- 左值可以被取地址,也可以被修改(const修饰的左值除外,也就是说const修饰的左值也是左值)。
- 左值可以出现在赋值符号的左边,也可以出现在赋值符号的右边。
右值
右值也是一个表示数据的表达式,比如:字面常量,表达式返回值,函数返回值
右值可以出现在赋值符号的右边,右值不能出现在赋值符号的左边,右值不能取地址
右值引用就是对右值的引用,给右值取别名
C++11对右值进行了严格的区分:
🌿 纯右值,比如:a+b, 100
🌿 将亡值。比如:表达式的中间结果、函数按照值的方式进行返回。
//常见的右值
//纯右值
10;
x+y;
//将亡值
fmin(x,y)
int& GetG_A()
{
return g_a;//g_a就是一个将亡值虽然当下是一个左值,但是会在返回过程中变成临时变量,就成了右值,属于编译器的转化
}
注:
fmin()
的返回是一个传值返回,所以返回值先通过一个临时变量保存返回
🌸 右值本质就是一个临时变量或常量值,比如代码中的10就是常量值,表达式x+y和函数fmin的返回值就是临时变量,这些都叫做右值。
🌸 这些临时变量和常量值并没有被实际存储起来,这也就是为什么右值不能被取地址的原因,因为只有被存储起来后才有地址。
🌸 但需要注意的是,这里说函数的返回值是右值,指的是传值返回的函数,因为传值返回的函数在返回对象时返回的是对象的拷贝,这个拷贝出来的对象就是一个临时变量。
总结一下就是:对于右值来说
- 不能出现取地址
- 不能出现在=的左边,不能修改
//这里编译会报错:error C2106: “=”: 左操作数必须为左值
10 = 1;
x + y = 1;
fmin(x, y) = 1;
区分左值和右值
关于左值与右值的区分不是很好区分,一般认为:
🍁 普通类型的变量,因为有名字,可以取地址,都认为是左值。
🍁 const修饰的常量,不可修改,只读类型的,理论应该按照右值对待,但因为其可以取地址(如果只是const类型常量的定义,编译器不给其开辟空间,如果对该常量取地址时,编译器才为其开辟空间),C++11认为其是左值。
🍁 如果表达式的运行结果是一个临时变量或者对象,认为是右值。
🍁 如果表达式运行结果或单个变量是一个引用则认为是左值。
左值引用与右值引用
传统的C++语法中就有引用的语法,而C++11中新增了右值引用的语法特性,为了进行区分,于是将C++11之前的引用就叫做左值引用。
首先我们明确:
无论是左值引用还是右值引用,都是给对象取别名
普通引用只能引用左值,不能引用右值,const引用既可引用左值,也可引用右值。
C++11中右值引用:只能引用右值,一般情况不能直接引用左值。
左值引用
int main()
{
//以下的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;
}
左值引用就是对左值的引用,给左值取别名,通过“&”来声明。
右值引用
右值引用就是队右值的引用,给右值取别名
// 以下几个都是常见的右值
10;
x + y;
fmin(x, y);
// 以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
一般右值都是临时的,没有被存起来的,所以这些变量都是不能被取地址的
但是一旦给右值取别名之后,会导致右值被存储到特定的位置,且可以取到该位置的地址,也就是,我们不能取10的地址,但是我们可以右值引用,右值引用完的rr1,可以修改和取地址。(如果不想被修改,可以用const修饰右值引用,一般不会用到该特性,一点不重要)
rr1 = 20;
左值引用引用右值
左值引用可以引用右值吗? 答:不可以,但不是完全不可以😂
左值引用 | 引用 | 左/右值 | 可以/不可以 |
---|---|---|---|
左值引用 | & | 左值 | ✔️ |
左值引用 | & | 右值 | ❌ |
const左值引用 | const & | 右值 | ✔️ |
左值引用不能直接引用右值
int& ra2 = 10 //编译失败,因为10是右值
但是const修饰可以引用
const int& ra2 = 10;
const int& ra3 = 10 + 20;
右值引用引用左值
右值引用可以引用左值吗? 答:不可以,但不是完全不可以😂
右值引用 | 引用 | 左/右值 | 可以/不可以 |
---|---|---|---|
右值引用 | && | 右值 | ✔️ |
右值引用 | && | 左值 | ❌ |
右值引用+move | &&+std::move | 左值 | ✔️ |
右值引用不能直接引用左值
// error C2440: “初始化”: 无法从“int”转换为“int &&”
// message : 无法将左值绑定到右值引用
int a = 10;
int&& r2 = a;
但是C++库中提供了move
按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。C++11中,std::move()函数位于 头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。
注意:
- 被转化的左值,其生命周期并没有随着左值的转化而改变,即std::move转化的左值变量lvalue不会被销毁。
- STL中也有另一个move函数,就是将一个范围中的元素搬移到另一个位置。
大致的move的定义是
template<class _Ty>
inline typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT
{
//forward _Arg as movable
return ((typename remove_reference<_Ty>::type&&)_Arg);
}
move也不能随便使用,像上面的s4强制使用move将左值转化为右值,导致了s1的值被swap,需要注意
左值引用的短板
既然C++98中的const类型引用左值和右值都可以引用,那为什么C++11还要复杂的提出右值引用呢?
右值引用的目的是为了补齐左值引用的短板
我们知道左值引用的两个场景是
- 做返回值
- 做引用传参
如果一个类中涉及到资源管理,用户必须显式提供拷贝构造、赋值运算符重载以及析构函数,否则编译器将会自动生成一个默认的,如果遇到拷贝对象或者对象之间相互赋值,就会出错,比如:
namespace allen
{
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
//cout << "string(char* str)" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// s1.swap(s2)
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
// 赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void push_back(char ch)
{
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
//string operator+=(char ch)
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
const char* c_str() const
{
return _str;
}
private:
char* _str;
size_t _size;
size_t _capacity; // 不包含最后做标识的\0
};
allen::string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
allen::string str;
while (value > 0)
{
int x = value % 10;
value /= 10;
str += ('0' + x);
}
if (flag == false)
{
str += '-';
}
std::reverse(str.begin(), str.end());
return str;
}
}
类似于to_string这样的函数,是不可以用作引用返回的
在operator+中:strRet在按照值返回时,必须创建一个临时对象,临时对象创建好之后,strRet就被销毁了,最后使用返回的临时对象构造s3,s3构造好之后,临时对象就被销毁了。仔细观察会发现:strRet、临时对象、s3每个对象创建后,都有自己独立的空间,而空间中存放内容也都相同,相当于创建了三个内容完全相同的对象,对于空间是一种浪费,程序的效率也会降低,而且临时对象确实作用不是很大,那能否对该种情况进行优化呢?
右值引用和生命周期
为了方便对临时对象的使用,C++ 对临时对象有特殊的生命周期延长规则。这条规则是:如果一个 prvalue (纯右值)被绑定到一个引用上,它的生命周期则会延长到跟这个引用变量一样长。
需要万分注意的是,这条生命期延长规则只对 prvalue 有效,而对 xvalue 无效。如果由于某种原因,prvalue 在绑定到引用以前已经变成了 xvalue(将亡值),那生命期就不会延长。
所以需要依靠右值引用提高效率,但也不是直接使用右值引用就能轻轻松松解决的
移动语义
C++11提出了移动语义概念,即:将一个对象中资源移动到另一个对象中的方式,可以有效缓解该问题。
在C++11中如果需要实现移动语义,必须使用右值引用。
在使用容器类的情况下,移动更有意义。
移动构造
移动构造是一个构造函数,该构造函数的参数是右值引用类型的,移动构造本质就是将传入右值的资源窃取过来,占为己有,这样就避免了进行深拷贝,所以它叫做移动构造,就是窃取别人的资源来构造自己的意思。
// 移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
this->swap(s);
}
因为strRet对象的生命周期在创建好临时对象后就结束了,即将亡值,C++11认为其为右值,在用strRet构造临时对象时,就会采用移动构造,即将strRet中资源转移到临时对象中。而临时对象也是右值,因此在用临时对象构造s时,也采用移动构造,将临时对象中资源转移到s中,整个过程,只需要创建一块堆内存即可,既省了空间,又大大提高程序运行的效率。
注意:
- 移动构造函数的参数千万不能设置成const类型的右值引用,因为资源无法转移而导致移动语义失效。
- 在C++11中,编译器会为类默认生成一个移动构造,该移动构造为浅拷贝,因此当类中涉及到资源管理时,用户必须显式定义自己的移动构造。
移动构造和拷贝构造的区别:
🍁 在没有增加移动构造之前,由于拷贝构造采用的是const左值引用接收参数,因此无论拷贝构造对象时传入的是左值还是右值,都会调用拷贝构造函数。
🍁 增加移动构造之后,由于移动构造采用的是右值引用接收参数,因此如果拷贝构造对象时传入的是右值,那么就会调用移动构造函数(最匹配原则)。
🍁 string的拷贝构造函数做的是深拷贝,而移动构造函数中只需要调用swap函数进行资源的转移,因此调用移动构造的代价比调用拷贝构造的代价小。
虽然to_string当中返回的局部string对象是一个左值,但由于该string对象在当前函数调用结束后就会立即被销毁,我可以把这种即将被消耗的值叫做“将亡值”,比如匿名对象也可以叫做“将亡值”。
既然“将亡值”马上就要被销毁了,那还不如把它的资源转移给别人用,因此编译器在识别这种“将亡值”时会将其识别为右值,这样就可以匹配到参数类型为右值引用的移动构造函数。
C++11后是增加了移动构造,编译器在看到右值的时候,又发现类中又有拷贝构造又有移动构造的时候,会执行移动构造,直接转移到ret1,这个过程中就没有了深拷贝
allen::string ret1 = allen::to_string(1234);
allen::to_string(1234);
不论使用的时候有没有值接收,只要有移动构造,都会调用移动构造
很多容器库里面都是提供移动构造的
编译器的优化
实际当一个函数在返回局部时,会先用这个局部对象拷贝构造出一个临时对象,然后再用这个临时对象来拷贝构造我们接收返回值的对象。如下:
因此在C++11标准出来之前,对于深拷贝的类来说这里就会进行两次深拷贝,所以大部分编译器为了提高效率都对这种情况进行了优化,这种连续调用构造函数的场景通常会被优化成一次。
C++98做的是编译器的优化,原来要拷贝两次,现在是一次(并不是所有的编译器都做了这个优化)
在C++11出来之后,编译器的这个优化仍然起到了作用。
如果编译器不优化这里应该调用两次移动构造,第一次调用移动构造用返回的局部string对象构造出一个临时对象,第二次调用移动构造用这个临时对象构造接收返回值的对象。
而经过编译器优化后,最终这两次移动构造就被优化成了一次,也就是直接将返回的局部string对象的资源移动给了接收返回值的对象。
此外,C++11之后就算编译器没有进行这个优化问题也不大,因为不优化也就是调用两次移动构造进行两次资源的转移而已。
但是:
但如果我们不是用函数的返回值来构造一个对象,而是用一个之前已经定义出来的对象来接收函数的返回值,这时编译器就无法进行优化了。比如:
这时当函数返回局部对象时,会先用这个局部对象拷贝构造出一个临时对象,然后再调用赋值运算符重载函数将这个临时对象赋值给接收函数返回值的对象。
🍊 编译器并没有对这种情况进行优化,因此在C++11标准出来之前,对于深拷贝的类来说这里就会存在两次深拷贝,因为深拷贝的类的赋值运算符重载函数也需要以深拷贝的方式实现。
🍊 但在深拷贝的类中引入C++11的移动构造后,这里仍然需要再调用一次赋值运算符重载函数进行深拷贝,因此深拷贝的类不仅需要实现移动构造,还需要实现移动赋值。
这里需要说明的是,对于返回局部对象的函数,就算只是调用函数而不接收该函数的返回值,也会存在一次拷贝构造或移动构造,因为函数的返回值不管你接不接收都必须要有,而当函数结束后该函数内的局部对象都会被销毁,所以就算不接收函数的返回值也会调用一次拷贝构造或移动构造生成临时对象。
移动赋值
移动赋值是一个赋值运算符重载函数,该函数的参数是右值引用类型的,移动赋值也是将传入右值的资源窃取过来,占为己有,这样就避免了深拷贝,所以它叫移动赋值,就是窃取别人的资源来赋值给自己的意思。
在当前的string类中增加一个移动赋值函数,该函数要做的就是调用swap函数将传入右值的资源窃取过来,为了能够更好的得知移动赋值函数是否被调用
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
this->swap(s);
return *this;
}
移动赋值和原有operator=函数的区别:
🍋 在没有增加移动赋值之前,由于原有operator=函数采用的是const左值引用接收参数,因此无论赋值时传入的是左值还是右值,都会调用原有的operator=函数。
🍋 增加移动赋值之后,由于移动赋值采用的是右值引用接收参数,因此如果赋值时传入的是右值,那么就会调用移动赋值函数(最匹配原则)。
🍋 string原有的operator=函数做的是深拷贝,而移动赋值函数中只需要调用swap函数进行资源的转移,因此调用移动赋值的代价比调用原有operator=的代价小。
现在给string增加移动构造和移动赋值以后,就算是用一个已经定义过的string对象去接收to_string函数的返回值,此时也不会存在深拷贝。
此时当to_string函数返回局部的string对象时,会先调用移动构造生成一个临时对象,然后再调用移动赋值将临时对象的资源转移给我们接收返回值的对象,这个过程虽然调用了两个函数,但这两个函数要做的只是资源的移动,而不需要进行深拷贝,大大提高了效率。
说明一下: 在实现移动赋值函数之前,该代码的运行结果理论上应该是调用一次拷贝构造,再调用一次原有的operator=函数,但由于原有operator=函数实现时复用了拷贝构造函数,因此代码运行后的输出结果会多打印一次拷贝构造函数的调用,这是原有operator=函数内部调用的。
STL也实现了移动赋值
总结一下:
右值引用出来以后,并不是直接使用右值引用去减少拷贝,提高效率。而是支持深拷贝的类,提供移动构造和移动赋值这时这些类的对象进行传值返回或者是参数为右值时,则可以用移动构造和移动赋值,转移资源,避免深拷贝,提高效率。
右值引用+容器
容器中的insert和push_back等为了提高效率增加了右值引用
list<allen::string> lt;
allen::string s1("1111");
// 这里调用的是拷贝构造
lt.push_back(s1);
// 下面调用都是移动构造
lt.push_back("2222");
lt.push_back(allen::string("2222"));
lt.push_back(std::move(s1));
总结一下:右值引用使用场景二,还可以使用在容器插入接口函数中,如果实参是右值,则可以转移它的资源,减少拷贝
完美转发
完美转发是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。
模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,
但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,
如果我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用完美转发
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
template<typename T>
void PerfectForward(T&& t)
{
Fun(std::forward<T>(t));
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
PerfectForward为转发的模板函数,Func为实际目标函数,但是上述转发还不算完美,完美转发是目标函数总希望将参数按照传递给转发函数的实际类型转给目标函数,而不产生额外的开销,就好像转发者不存在一样。
所谓完美:函数模板在向其他函数传递自身形参时,如果相应实参是左值,它就应该被转发为左值;如果相应实参是右值,它就应该被转发为右值。这样做是为了保留在其他函数针对转发而来的参数的左右值属性进行不同处理(比如参数为左值时实施拷贝语义;参数为右值时实施移动语义)。
完美转发的作用
正是因为右值引用的对象,在作为实参传递时,属性会退化为左值/只能匹配左值引用(可以被取地址了,所以退化为左值)。使用完美转发,可以保持他的右值属性
void PushBack(const T& x)
{
Insert(_head, x);
}
void PushBack(T&& x)
{
// 这里x属性退化为左值,其他对象再来引用x,x会识别为左值
// 这里就要用完美转发,让x保持他的右值引属性
Insert(_head, std::forward<T>(x));
}
只要有传参,一定要完美转发,尤其像下面的
void Insert(Node* pos, T&& x)
{
Node* prev = pos->_prev;
//Node* newnode = new Node;
//newnode->_data = std::forward<T>(x); // 关键位置
Node* newnode = (Node*)malloc(sizeof(Node));
new(&newnode->_data)T(std::forward<T>(x));
// prev newnode pos
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = pos;
pos->_prev = newnode;
}
这里使用到了技术:定位new
那么STL中也是这样的
参考资料:
https://blog.csdn.net/chenlong_cxy/article/details/126747523