一、右值引用
1、区分左值和右值
语法定义,左值可以取地址,右值无法取地址(右值肯定有地址,但是为了和左值区分,语法上不让取地址)
左值:一个表示数据的表达式(变量名或解引用指针)
右值:一个表示数据的表达式(字面常量,临时对象,匿名对象)
下图的右值无法取地址
注意:
左值引用不能给右值取别名,本质右值无法改变,所以 const 左值引用可以。
const int& a = 10;
右值引用无法给左值取别名,但是move之后的左值可以,相当于把左值变成右值。
int&& b = move(10);
2、引用解决的问题
引用的意义:减少拷贝,提高效率
(1)左值引用解决的问题和遗留的问题
引用传参防止拷贝,传引用返回减少拷贝
但如果是局部对象传引用返回,由于局部对象出函数销毁,导致必须经历深拷贝构造新的临时对象返回,最后没有达到引用返回的目的。
(2)右值引用解决左值引用问题的原理
右值分类和解决原理
右值分为两大类:纯右值(内置类型),将亡值(自定义类型,包括生命周期一行的匿名对象和生命周期是表达式的临时对象)
内置类型不作考虑,因为本身拷贝代价就极低。主要考虑自定义类型的拷贝构造和赋值构造能否基于右值特性进行优化。
我们发现不论是匿名对象还是临时对象都有一个特点,他们有我们构造对象所需要的资源,但是占有资源的主体生命周期短,是临时的很快就销毁(所以叫将亡值),所以此时我们可以直接交换资源,把我新创建的对象指向的资源和将亡值指向的资源进行交换,这样代价就比左值返回代价小的多。
上文提到可以通过改造左值引用的拷贝构造和赋值构造来解决问题。用右值特性改造之后的函数叫移动构造和移动赋值,听名字移动成本就很低。
举例
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& ss)
{
::swap(_str, ss._str);
::swap(_size, ss._size);
::swap(_capacity, ss._capacity);
}
// 拷贝构造
// 左值
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
// 移动构造 -- 移动将亡值对象的资源
// 右值(将亡值)
string(string&& s)
:_str(nullptr)
{
cout << "string(string&& s) -- 移动构造" << endl;
swap(s);
}
// 赋值重载 左值
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
char* tmp = new char[s._capacity + 1];
strcpy(tmp, s._str);
delete[] _str;
_str = tmp;
_size = s._size;
_capacity = s._capacity;
return *this;
}
// 移动赋值 右值
string& operator=(string&& s)
{
cout << "string(string&& s) -- 移动赋值" << endl;
swap(s);
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
};
bit::string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
bit::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 move(str);
return str;
}
这是一个自己写的string类,里面就有加上移动构造和移动赋值。
移动构造解析
首先要用右值引用来接受参数,让编译器知道如果有右值要构造用移动构造函数。
内部让 this 指针指向的对象和将亡值进行交换资源就达到了构造新对象的目的。
复制构造解析
移动赋值也是如此,你将亡值里面有我对象所需要的资源,我就直接交换资源,函数结束后不久将亡值就会带着交换后的没用资源一起被释放。
代码案例
int main()
{
//bit::string ret1;
//ret1 = bit::to_string(1234);
bit::string ret1 = bit::to_string(1234);
cout << ret1.c_str() << endl;
return 0;
}
没有移动构造和移动赋值:
可以看到需要两次深拷贝,代价很大。
有移动构造和移动赋值:
还可以再优化:
内存级理解:
int main()
{
bit::string ret1;
ret1 = bit::to_string(1234);
//bit::string ret1 = bit::to_string(1234);
//cout << ret1.c_str() << endl;
return 0;
}
没有移动构造和移动赋值:
依然两次深拷贝。
有移动构造和移动赋值:
内存级理解:
3、右值引用总结
首先内置类型没有移动语义,因为他们的创建代价极低。
右值引用针对的是深拷贝的自定义类型,因为如果只是单纯的左值引用无法解决深拷贝。
右值引用本身存在效率问题,只是函数匹配时编译器会识别到右值,此时右值是一个将亡值,又具有我们需要的资源,此时的移动构造和移动赋值可以直接转移资源提高效率。
其次右值引用本身是左值,只有这样移动构造和移动赋值才能交换资源(为了逻辑自洽)
所以让右值变成左值的方法两种:右值引用,强转。
二、完美转发
1、函数模板中的万能引用
template<typename T>
void PerfectForward(T&& t)
{}
参数 T&& t,不是代表右值引用,因为在模板中,所以是万能引用,即实参传左值推算成左值引用,实参传右值推算成右值引用。
所以我们写一个函数对万能引用进行测试。
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(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;
}
理论上我们经过万能引用会得到main函数中注释的结果。
但是要知道右值被万能引用函数捕捉后的右值引用是左值,这样做导致运行结果全是左值。
所以我们该如何解决函数传参时上述的属性退化问题呢?
2、完美转发理解
要想在参数传递过程中保持其右值属性,就需要使用 forward 函数,也就是完美转发。
forward 是一个带有参数模板的函数,主要在传参时使用: 如果参数原本是右值,但在右值引用后失去了右值属性,使用 forward 函数可以恢复它的右值属性。
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;
}
//forward<T>(t)在传参过程中保持t的原生类型属性,叫做完美转发
template<typename T>
void PerfectForward(T&& t)
{
//Fun(t);
Fun(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;
}
所以此时的运行结果就i是我们想要的了。
3、完美转发使用场景
完美转发在实际开发中会经常用到,前面说过,在 C++11
之后,所有的类都可以新增一个移动构造以规避无意义的低效拷贝行为,并且由于大部分类中会涉及模板的使用,保持右值属性就是一个必备的技巧,如果没有完美转发,那么移动构造顶多也就减少了一次深拷贝。
通俗来说就是不是每一层函数的传参都能保证参数的右值属性,这是就有必要用到完美转发。
三、lambda表达式
1、仿函数
借助类和 operator() 函数重载来创建函数对象,用于各种特定函数传参。
struct cmpLess
{
bool operator()(int n1, int n2)
{
return n1 < n2;
}
};
struct cmpGreater
{
bool operator()(int n1, int n2)
{
return n1 > n2;
}
};
int main()
{
vector<int> arr = { 8,5,6,7,3,1,1,3 };
sort(arr.begin(), arr.end(), cmpLess()); // 升序
sort(arr.begin(), arr.end(), cmpGreater()); // 降序
return 0;
}
例如sort()函数会要求传递一个比较函数用于排序,可以使用默认或库里面自带的仿函数,但是也可以自己实现一个仿函数来实现特定的排序需求。这时我们就要会写仿函数来实现。
不仅是sort函数,优先级队列也需要自己写仿函数传参。
2、lambda表达式作用
上面的仿函数写起来不免有点麻烦,lamdba表达式功能与仿函数类似,但是更简洁。
landba表达式会生成匿名函数对象,在编译时会生成仿函数,即调用opreator(),这些时编译器干的活。
3、lamdba表达式语法
格式:[capture-list] (parameters) mutable -> return-type { statement }
1、[capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来 判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda 函数使用。
捕捉类型
[value]:传值捕捉value
[&value]:传引用捕捉value
[=]:传值捕捉所有变量,包括this指针
[&]:传引用捕捉所有变量,包括this指针
[&,value1,value2]:混合捕捉,除value1,value2以外的变量以传引用来捕捉
2、(parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以 连同()一起省略
3、mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量 性。使用该修饰符时,参数列表不可省略(即使参数为空)。
4、->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回 值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
5、{statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
其中参数,mutable,->returntype 可以省略,捕捉列表和函数体内容可以为空,但是不能省略
4、lamdba表达式应用
int main()
{
int a = 1;
int b = 2;
//参数表是引用捕捉,最后结果a = 2 b = 1
auto swap1 = [](int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
};
//本质是捕捉拷贝,最后结果是a = 1 b = 2
auto swap2 = [a, b]() mutable
{
int tmp = a;
a = b;
b = tmp;
};
//引用捕捉,最后结果是a = 2 b = 1
auto swap3 = [&a, &b]()
{
int tmp = a;
a = b;
b = tmp;
};
return 0;
}
由于lamdba表达式返回值类型只有编译器知道,本质产生的是对象,所以用 auto 接受。
四、深入理解c++11之后的默认成员函数
c++11之后新增了移动构造函数和移动赋值运算符重载,这两个都是默认成员函数。
规则
1、若没有实现移动构造或移动赋值,并且没有实现析构函数,拷贝构造,复制构造的任意一个函数,编译器就会默认生成移动构造或移动赋值。
注意:虽然是说 “没有实现析构函数,拷贝构造,复制构造的任意一个函数” 其实这三个函数是一起的,你写了析构函数就证明一定有深拷贝的成员变量需要释放,那么此时拷贝构造,复制构造就是有必要写的。
2、默认生成的移动构造或移动赋值函数,对于内置类型按字节拷贝,自定义类型看他是否有移动构造或移动赋值函数,如果有就调用,没有就走拷贝构造或赋值重载函数。
3、如果提供了移动构造或移动赋值,编译器不会提供拷贝构造或赋值重载函数。
4、所以写了析构函数,拷贝构造,复制构造的任意一个函数,编译器就不会默认生成移动构造或移动赋值,我们只能自己写移动构造和移动赋值,此时根据结论3,我们不写拷贝构造或赋值重载函数就会报错。
5、根据结论4总结就是我们可以只写析构函数,拷贝构造,复制构造,但是如果要写移动构造或移动赋值,就必须析构函数,拷贝构造,复制构造三个函数一个不少。
6、函数如果不期望被调用就在该函数声明加上=delete 即可。