引用
左值和右值
左值:lvalue,一个在内存中占有确定位置的对象(换句话说就是有一个地址)
右值:与左值相反,没有内存位置,临时对象
比如下面的代码:
int i = 4;
i为左值,4为右值,因此
下面是错误的:
4 = i;
左值引用和右值引用
左值引用:对左值的引用,保存左值的地址
右值引用:对右值的引用
如下所示:
int a = 4;
int& b = a; // 将左值a绑定到左值引用b上
int&& c= a + 4; // 将右值a+4绑定到右值引用c上
下面是错误的:
int a = 4;
int &b = a + 4; // 错误, a+4是右值
int &&c = a; // 错误,a是左值
但是右值引用有什么作用呢?
右值引用的作用
根据右值的定义,可以推断出右值有以下特性:
- 所引用的对象将要被销毁
- 该对象没有其他用户
所以,右值引用的作用便是 当对象的资源不再被原先对象需要时,可以唯一由右值引用负责。换句话说,
使用右值引用的代码可以自由的接管被引用对象的资源。
移动
上面代码举例中展示了左值不能绑定到右值引用上,但是可以通过move函数,显示的将一个左值转化为对应的右值引用,并剥夺原先左值占用对象的权力,如下所示:
string st = "I love xing";
vector<string> vc ;
vc.push_back(move(st));
cout<<vc[0]<<endl; // 不输出任何东西
if(!st.empty())
cout<<st<<endl; // 输出 I love xing
移动构造函数
拷贝构造函数不同的是,移动构造函数接受的是右值引用而非左值引用,并且经过移动构造函数,被移动的对象的资源将被”窃取“掉。在完成资源的移动之后,源对象将不在拥有任何资源,其资源所有权已经转交给新创建的对象。
举例如下:
#include <cstring>
#include <iostream>
class MyString {
private:
char* string;
public:
MyString() : string(nullptr) {}
MyString(const char* str) {
// 这里采用深拷贝
string = (char*)malloc(strlen(str) + 1);
strcpy(string, str);
std::cout << "I'm constructor of class MyString" << std::endl;
}
MyString(const MyString& mystr) {
// 这里同样是深拷贝,mystr 任然持有它自己的资源
string = (char*)malloc(strlen(mystr.string) + 1); // 为 string 分配新的资源
strcpy(string, mystr.string);
std::cout << "I'm copy constructor of class MyString" << std::endl;
}
MyString(MyString&& mystr) noexcept : string(mystr.string) {
// 注意!移动构造函数,这里 mystr 已经不再持有任何资源
// mystr.string 所指向的资源已经被当前对象窃取
// 这里切记要将被移动的资源的指针置为空,为了防止析构函数析构其已经被转移的资源
mystr.string = nullptr;
std::cout << "I'm move constructor of class MyString" << std::endl;
}
~MyString() {
std::cout << "I'm destructor of class MyString" << std::endl;
if (string) {
// 如果 string 指针还持有资源的话,就将其释放
free(string);
std::cout << "free string!" << std::endl;
}
}
};
int main1(int argc, char* argv[]) {
MyString s1("hello world");
MyString s2(s1); // 调用MyString(const MyString& mystr)
//MyString s3(std::move(s1)); // 调用MyString(MyString&& mystr) noexcept : string(mystr.string)
std::cout << std::endl;
return 0; // 函数退出时两次析构,两次free
}
int main2(int argc, char* argv[]) {
MyString s1("hello world");
//MyString s2(s1); // 调用MyString(const MyString& mystr)。
MyString s3(std::move(s1)); // 调用MyString(MyString&& mystr) noexcept : string(mystr.string)
std::cout << std::endl;
return 0; // 函数退出时两次析构,一次free
}
注意
如果没有定义移动构造函数,但是实际代码中用到了move,则默认会调用类的拷贝构造函数;
如果定了移动构造函数而没有定义拷贝构造函数,则编译器不会自动生成拷贝构造函数,即相当于把拷贝构造函数定义为=delete;
转发
在我们调用函数的时候,会把实参传递给函数,有时候我们传给函数的是左值,有时候给的是右值,有时候还可能给的是 const 类型。在这些情况下,我们要求函数接收参数后,依然能保持这些类型,这时候就需要用转发。
比如下面的例子:
// 首先定义一个函数模板
template <typename F, typename T1, typename T2>
void middle1(F f, T1 t1, T2 t2) {
f(t1, t2);
}
// 定义一个函数
void f(int v1, int& v2) { // v2 是一个引用
++v1;
++v2;
}
// main函数中直接调用f(int v1, int& v2) 通过函数模板middle1(F f, T1 t1, T2 t2)调用f
int main(int argc, char* argv[]) {
int i = 0;
f(42, i);
cout << "After call the f directly: " << i << endl; // 输出 1
middle1(f, 42, i);
cout << "Call f through middle1: " << i << endl; // 输出 1
}
上面的例子可以看出,通过函数模板调用f,i并没有被+1,当我们将 i 绑定到 middle 的参数 t2 上后,传给 f 的是 t2,而 t2 只是一个普通的、非引用的类型 int,而不是对 i 的引用。所以 i 的值并没有改变。
保留类型信息
通过将函数模板定义如下,便可以实现i继续+1
template <typename F, typename T1, typename T2>
void middle2(F f, T1&& t1, T2&& t2) {
f(t1, t2);
}
这样便可以实现i自增两次。在 middle2 中,实参的“左值性“得到了保留。这就要归功于 引用折叠 了,简单说一下引用折叠:
-
X& &、X& && 和 X&& & 都会折叠成类型 X&
-
类型 X&& && 折叠成 X&&
但是考虑如下函数:
void g(int &&v1, int& v2) {
++v1;
++v2;
}
int main(int argc, char* argv[]) {
int i = 0;
middle2(g, 42, i); // 会报错,无法将一个右值引用绑定到左值上。
cout << "Call g through middle2: " << i << endl;
}
为什么编译器会提示无法将一个右值引用绑定到左值上?因为 g 的参数 v1 是右值引用,将 42 给右值引用完全没问题,但问题就出在,虽然 42 是右值,但 t1 却不是,变量都是左值,即使是右值引用,但也是变量,所以还是左值,而左值是无法与右值引用绑定,即发生了如下的错误:
int&& a = 4;
int&& b = a; // 错误,a为右值引用,但是也是一个变量,是一个左值。
std::forward函数
foward 要明确给出模板参数:std::foward(i) 才能使用。forward 会返回模板参数类型的右值引用,也就是:int&& 。注意,如果我们这样使用:std::forward<int&>(i) 返回的就是 int& && ,进而折叠为 int& 。
因此,对于上面的g函数,可以通过以下方法保留类型信息:
template <typename F, typename T1, typename T2>
void middle3(F f, T1&& t1, T2&& t2) {
f(std::forward<T1>(t1), std::forward<T2>(t2));
}
void g(int&& i, int& j) {
++i;
++j;
}
int main(int argc, char* argv[]) {
int i = 0;
middle3(g, 42, i);
cout << "Call g through middle3: " << i << endl; // 输出 2
}
上面两个函数参数发生的引用折叠为:
std::forward(t1):int&& && -> int&&
std::forward(t2):int& && -> int&
可以看出,左值引用和右值引用都得到了保留。
ref
- https://blog.csdn.net/lws123253/article/details/80353197
- https://guodong.plus/2020/0314-132811/