目录
一. 左值和右值的概念
1. 左值
1.1 可修改的的左值
1.2 不可修改的左值
右值
二. 左值引用和右值引用
1. 左值引用
2. 右值引用
主要用途
1. 移动语义
2. 完美转发
2.1 引用折叠
2.2 std::forward
一. 左值和右值的概念
什么是左值和右值
1. 左值
左值是一个表示数据的表达式,它代表一个具名的内存位置,程序可以获取其地址,可以通过地址访问它们,是可被引用的数据对象。左值又可分为可修改的的左值和不可修改的左值。
1.1 可修改的的左值
一开始,左值其实就是指可出现在赋值语句左边的表达式,它们的值是可以被修改的,当然这是最初的概念了。比如常规变量名、解除引用的指针、数组元素、引用、对象成员都是左值。
int a;
a = 10;
int* ptr = &a;
*ptr = 100;
vector<int> vec(5,10);
vec[2] = 20;
int& c = a;
c = 1000;
1.2 不可修改的左值
后面随着关键字const的引入,这个左值的概念发生了变化 。因为虽然不能对const变量赋值,但是可以获取其地址。
const int a = 10;
const int* ptrA = &a; //可以通过地址访问它
a = 100; //错误
-
右值
前面说完了左值,那右值是什么呢?通过排他性来定义的话,每个表达式不是左值就是右值。 右值代表一个临时的值,不能被取地址,不能被修改,是不能出现在赋值语句的左边的。
右值包括字面常量(C-style字符串除外,它表示地址)、临时对象、诸如x+y等表达式、返回值的函数(条件是该函数返回的不是引用)等。
int foo() {
return 1;
}
void test01() {
int a = 1;
int b = 2;
int c = a + b;
int d = foo();
};
比如上面的a,b,c,d这几个变量都是左值,但是a+b和foo()都是右值。 我们知道是无法写成如下这样的:
a + b = 3;
foo() = 2;
二. 左值引用和右值引用
1. 左值引用
左值引用也就是传统的C++引用,通过&符号来声明,它可以使得标识符关联到左值。引用是已定义变量的别名,例如下面将b作为a变量的别名,b和a指向相同的值和内存单元
int a = 1;
int& b = a;
引用必须在创建时就要进行初始化,一旦与某个变量关联起来,就将一直相关联,不会中途关联到其他变量
int a = 1;
//错误,因为引用必须在创建时就关联变量
int& b;
b = a;
引用的主要用途是用来作为函数的形参,通过将引用变量用作参数,函数将使用原始数据,而不是其副本。
2. 右值引用
在C++11引入了右值引用(rvalue reference)的概念,通过&&符号来声明,右值引用可以关联到右值。
int && a = 1;
cout << "a = " << a << ", " << &a<< endl;
-
主要用途
引入右值引用的目的是什么呢?其主要目的之一是实现移动语义。
1. 移动语义
移动语义是一种优化技术,它允在对象之间转移资源的所有权,而不是进行资源的复制或者拷贝。移动语义这种技术正是通过使用右值引用来实现的。
先说一下为什么会诞生移动语义这个东西:当调用拷贝构造函数或赋值运算符时会对对象进行拷贝操作,也就是会申请一块新的内存空间,然后把数据复制到新的内存空间当中,但是这对于大型对象来说工作量是很大的,当然在一些情况下这是必要的操作,但是在某些场景下,可能其实不用额外拷贝一份,只需要将对象的资源所有权从一个对象转移到另一个对象就可以满足要求了。举一个例子(取自于C++ Primer Plus):
vector<string> allcaps(const vector<string>& vs) {
vector<string> temp;
//code that store an all-uppercase version of vs in temp(将vs转换成全大写版本存储在temp中)
return temp;
}
vector<string> vstr;
//build up a vecor of 20000 strings,each of 1000 characters(构建存储有20000个字符串的vector,每个字符串包含1000个字符)
vector<string> vstr_copy(allcaps<vstr>);
可以发现,allocaps()创建了对象temp,这个temp对象管理着20000000个字符;vector和string的拷贝构造函数创建这20000000字符的副本,然后程序再删除allocaps()返回的临时对象temp。这里面做了大量的无用功,这里先是将temp对象的值拷贝给vstr_copy,然后再删除temp对象;如果能够直接将temp对数据的所有权转让给vstr_copy也就是不将temp所管理的20000000个字符复制到新地方,而是保留在原来的地方,并将vstr_copy与之相关联,这样的话,工作量就省掉了很多,效率也就上来了。于是移动语义这个技术就出现了,它便是可以将对象的资源所有权从一个对象转移到另一个对象,而不进行资源的复制的技术。
移动语义得以实现,这其中便离不开右值引用的支持。那右值引用对移动语义的支持体现在什么地方呢:因为需要让编译器知道什么时候需要的是拷贝,什么时候不需要,这就是右值引用发挥作用的地方了。简单来说,就是编译器会根据会根据传进来的参数是左值引用还是右值引用来决定调用拷贝构造函数还是移动构造函数(移动构造函数里将资源从一个对象转移到另一个对象,而不用复制)。
class test {
public:
test(const test& t); //拷贝构造函数
test(test&& t); //移动构造函数
private:
int n;
};
总结,移动语义发生,需要两个步骤:
1. 右值引用让编译器知道何时可使用移动语义
2. 编译移动构造函数,使其提供所需的方法。也就是怎么让资源从一个对象转移到另一个对象,而不用复制
2. 完美转发
完美转发是C++11引入的一种特性,它是指泛型编程(模板编程)中,函数模板能够完全自己模板参数的类型传递给内部调用的其他函数,即参数左右值的属性不会发生变化。
完美转发能够保留参数的类型和值类别(左值或右值),从而实现更为高效且准确地传递参数,用于解决在函数模板中传递参数时的类型推导问题。它允许将参数以原始的形式传递给另一个函数,而不会引入额外的拷贝或移动操作。
完美转发的实现过程中有借助右值引用和std::forward。
2.1 引用折叠
C++中,一般右值引用的参数只能接收右值,不能接收左值;但是在函数模板中使用右值引用的参数又不太一样,它既可以接收右值引用,也可以接收左值引用。借助C++的引用折叠规则,只要函数模板的参数类型为 T&&,则 C++ 可以自行准确地判定出实际传入的实参是左值还是右值。
下面这个例子里,虽然模板函数f的参数t声明为了右值引用,但是实际传参是时可以是左值引用。这里“&&”又可成为通用引用
template<class T>
void f(T&& t) {
g(t);
};
假设用 A 表示实际传递参数的类型:
- 当实参为左值或者左值引用(A&)时,函数模板中 T&& 将转变为 A&(A& && = A&)
- 当实参为右值或者右值引用(A&&)时,函数模板中 T&& 将转变为 A&&(A&& && = A&&)
引用折叠规则:
& + & -> &
& + && -> &
&& + & -> &
&& + && -> &&可以发现,一旦定义中出现了左值引用,引用折叠规则总是优先将其折叠为左值引用。
2.2 std::forward
std::forward
是一个模板函数,用于在函数模板中将参数原封不动地转发给其他函数,保持参数的值类别(左值引用或右值引用的属性)和const/volatile限定符。
以前面那个例子,在函数模板f里调用函数g时,没有加forward,可以发现调用f时,即使传进去的参数是右值引用,但是从运行结果来看,f里调用函数g()时,传给函数g()的却变成了左值引用,这是因为传入给g()的是一个具名变量参数,具名变量即使被声明为右值类型也不会被当作右值,g()会认为这个值就是是一个左值;参数是左值,使用这个参数时还是会调用拷贝构造函数(如果是class的话)。
void g(int& a) {
cout << "左值引用" << endl;
}
void g(int&& a) {
cout << "右值引用" << endl;
}
template<typename T>
void f(T&& t) {
g(t);
};
void test01() {
f(2); //传入右值引用
return;
};
int main() {
test01();
return 0;
}
如果在调用函数g()时,改写成g(forward<T>(t))后,可以看到运行结果变成了预期的“右值引用”,这便是forward提供的完美转发的功劳。
void g(int& a) {
cout << "左值引用" << endl;
}
void g(int&& a) {
cout << "右值引用" << endl;
}
template<typename T>
void f(T&& t) {
g(forward<T>(t));
};
void test01() {
f(2); //传入右值引用
return;
};
参数t是一个通用引用,它可以接受左值引用或者左值引用的参数。通过使用std::forward,可以将原参数t封不动地转发给函数g,并保持参数t的左值引用或者左值引用属性不会发生变化。