本文总结 C++ 左右值的定义,和 C++11 中右值引用的用途。
转载:C++11新特性3 - 左右值与右值引用
C++ 11: Rvalue Reference – Move Sematics
C++ 11: Rvalue Reference – Fowarding
C++11通过引入右值引用来优化性能,通过移动语义来减少无谓的拷贝,通过完美转发来解决不能按照参数实际类型来
要点:
- 什么是右值:字面值和临时值/将亡值称为右值,或者说不能取地址的值称为右值
- 右值引用特点一:引用函数返回的将亡值,延长将亡值得生命周期,减少对象拷贝次数(const常量引用也能实现同样的效果,编译器返回值优化RVO能实现更好的效果(g++ -fno-elide-constructors关闭优化))
- 右值引用特点二:既可引用右值,又可引用左值,
- 右值引用特点三:T&& t是未定的引用类型(通用引用),是左值还是右值取决于它的初始化,可实现完美转发
1. 左值与右值
C++ 中,左右值的简化定义:
- 左值:占用了一定内存,且拥有可辨认的地址的对象
- 右值:左值以外的所有对象
典型的左值,C++中绝大部分的变量都是左值
int i = 1; // i 是左值
int *p = &i; // i 的地址是可辨认的
i = 2; // i 在内存中的值可以改变
class A;
A a; // 用户自定义类实例化的对象也是左值
典型的右值
int i = 2; // 2 是右值
int x = i + 2; // (i + 2) 是右值
int *p = &(i + 2);//错误,不能对右值取地址
i + 2 = 4; // 错误,不能对右值赋值
class A;
A a = A(); // A() 是右值
int sum(int a, int b) {return a + b;}
int i = sum(1, 2) // sum(1, 2) 是右值
引用(左值引用)
int i;
int &r = i;
int &r = 5; // 错误,不能左值引用绑定右值
// const 引用是例外
// 可以认为指向一个临时左值的引用,其值为5
const int &r = 5;
对于函数,类似地有
int square(int& a) {return a * a;}
int i = 2;
square(i); // 正确
square(2); // 报错,左值引用绑定右值
// 变通一下
int square(const int& a) {
return a * a;
}
// 可以调用 square(2)
左值可以创造右值, 右值也可创造左值
int i = 2;
int x = i + 2; // i + 2 是右值
int v[3];
// 指针解引用可以把右值转化为左值
*(v + 1) = 2;
一些注意事项
// 1. 函数也可以返回左值
int myglobal ;
int& foo() {return myglobal;}
foo() = 50;
// 一个常见的例子,[]操作符几乎总是返回左值
array[3] = 50;
// 2. 左值并不总是可修改的
// i是左值, 但不可修改
const int i = 1;
// 3. 右值有时是可修改的
class dog;
// bark() 可能会修改 dog() 对象
dog().bark();
2. 右值引用
右值引用的两大用途:
- 延长临时值生命周期
- 移动构造,浅拷贝
- 完美转发,区分左值右值
2.1 右值引用临时值
以下代码调用了几次A的构造函数:
A getA()
{
return A();
}
int main()
{
// 这句代码调用了几次A的构造函数(1次无参构造,2次拷贝构造)
A a1 = getA();
// 这句代码调用了几次A的构造函数(1次无参构造,1次拷贝构造)
const A& a2 = getA();
// 这句代码调用了几次A的构造函数(1次无参构造,1次拷贝构造)
A& a3 = getA();
}
右值引用能延长临时变量的声明周期,起到和常量左值引用类似的效果;但是常量左值引用是个常量,而右值引用不是常量,可以调用对象的const和非const函数。
2.2 移动语意与移动构造
类通常都会带有一个拷贝构造函数,但有时候我们又需要一个浅拷贝和一个深拷贝两个构造函数。尤其是拷贝构造的参数是个临时值时,我们通常希望只做浅拷贝,把成员变量的所有权移动至新对象,这就是所谓的移动语义,右值引用的一个重要作用就是用来支持移动语义。
如果从临时对象拷贝构造,希望调用浅拷贝;如果从左值对象拷贝构造,希望调用深拷贝:
以下代码编译时需禁用RVO(返回值优化):
// gcc -fno-elide-constructors
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <memory>
class A
{
public:
A()
{
printf("default construct %s:%d\r\n",__FUNCTION__,__LINE__);
}
A(const A&a) // 拷贝构造
{
if(a.str){
size_t len = strlen(a.str);
str = (char*)malloc(len+1);
memset(str,0,len+1);
if(len>0)
memcpy(str,a.str,len);
}
printf("copy construct %s:%d\r\n",__FUNCTION__,__LINE__);
}
A(A&&a) // 移动构造
{
str = a.str;
a.str = nullptr;
printf("move construct %s:%d\r\n",__FUNCTION__,__LINE__);
}
~A()
{
printf("%s:%d\r\n",__FUNCTION__,__LINE__);
if(nullptr != str)
{
free(str);
str = NULL;
}
}
char * str =nullptr;
};
A GetA()
{
A a ;
return a;
}
int main()
{
A a = GetA(); // 调用默认调用移动构造,GetA()返回的临时变量是右值,A a构造时调用移动构造
A a2 = a; // 调用拷贝构造,a是左值,A a2构造时调用拷贝构造
A a3 = std::move(a); // 显式调用移动构造,a 是左值,但想要把a的资源移动给a3,则显式调用std::move转为右值后再调用移动构造
// 展开后是 A a3 = A(std::move(a));
printf("ok\r\n");
}
2.2.1 std::move右值引用类型转换
在上面的移动构造函数中,参数必须是右值引用,如果一个左值也想要使用移动构造函数创建新的对象,该怎么办?
移动语意(std::move),可以将左值转化为右值引用。std::move等同于一个类型转换:static_cast<T&&>(lvalue)。
还是使用上面的代码测试:
int main()
{
A a = GetA(); // 调用移动构造
A a2 = a; // 调用拷贝构造
A a3 = std::move(a); // 调用移动构造,1. std::move(a) 返回a的右值引用;2. 以a的右值引用移动构造a3
A && a4 = std::move(a);
}
其他测试:
int a = 1; // 左值
int &b = a; // 左值引用
// 移动语意: 转换左值为右值引用
int &&c = std::move(a);
void printInt(int& i) {
cout << "lval ref: " << i << endl;
}
void printInt(int&& i) {
cout << "rval ref: " << i << endl;
}
int main() {
int i = 1;
// 调用 printInt(int&), i是左值
printInt(i);
// 调用 printInt(int&&), 6是右值
printInt(6);
// 调用 printInt(int&&),移动语意
printInt(std::move(i));
}
由于编译器调用时无法区分
- printInt(int) 与 printInt(int&)
- printInt(int) 与 printInt(int&&)
如果再定义 printInt(int) 函数,会报错
为什么需要移动语意
class myVector {
int size;
double* array;
public:
// 复制构造函数
myVector(const myVector& rhs) {
std::cout << "Copy Construct\n";
size = rhs.size;
array = new double[size];
for (int i=0; i<size; i++) {
array[i] = rhs.array[i];
}
}
myVector(int n) {
size = n;
array = new double[n];
}
};
void foo(myVector v) {
/* Do something */
}
// 假设有一个函数,返回值是一个 MyVector
myVector createMyVector();
int main() {
// Case 1:
myVector reusable=createMyVector();
// 这里会调用 myVector 的复制构造函数
// 如果我们不希望 foo 随意修改 reusable
// 这样做是 ok 的
foo(reusable);
/* Do something with reusable */
// Case 2:
// createMyVector 会返回一个临时的右值
// 传参过程中会调用拷贝构造函数
// 多余地被复制一次
// 虽然大部分情况下会被编译器优化掉
foo(createMyVector());
}
解决方法, 添加一个移动构造函数
// 移动构造函数
myVector(myVector&& rhs) {
std::cout << "Move Constructor\n";
size = rhs.size;
array = rhs.array;
rhs.size = 0;
rhs.array = nullptr;
}
那么,foo(createMyVector())就不会调用拷贝构造函数,而会调用移动构造函数 (当然更大的可能性是编译器直接把这一步优化掉)
然而,在 C++03 中,为了解决这个问题,可能需要定义两个 foo 函数,比较麻烦:
- foo_by_value(myVector)
- foo_by_ref(myVector&)
同时,假如我们在调用 foo 之后,reusable 就不再被使用了, 可以使用移动语意
int main () {
myVector reusable = createMyVector();
// 这里会调用 myVector 的移动构造函数
foo(std::move(reusable));
/* No use of reusable anymore */
}
另一个有用之处在于重载赋值运算符
X& X::operator=(X const & rhs);
X& X::operator=(X&& rhs);
自从 C++11,所有的 STL 都实现了移动构造
2.3 完美转发与引用折叠
我有一个需求,任务调度器需要将任务调度到不同的任务队列中;收到的任务可能是左值,也可能是右值;如果是左值,则拷贝构造后放到任务队列中;如果是右值,则移动构造后放入队列:
很容易就想到下面这段代码:
#include <stdio.h>
#include <utility>
// 任务
class Task{
public:
Task(){}
Task(const Task&){
printf("copy constract\n");
}
Task(Task&&){
printf("move constract");
}
};
// 任务队列
class TaskQueue{
public:
void AddTask(Task &){
printf("TaskQueue this is copy\n");
}
void AddTask(Task&&){
printf("TaskQueue this is move\n");
}
};
// 调度器
class Scheduler{
public:
void AddTask(Task &task,bool is_prioprity){
printf("Scheduler this is copy\n");
if(is_prioprity) priority_queue_.AddTask(task);
else second_queue_.AddTask(task);
}
void AddTask(Task &&task,bool is_prioprity){
printf("Scheduler this is move\n");
if(is_prioprity) priority_queue_.AddTask(task);
else second_queue_.AddTask(task);
}
private:
// 优先任务队列
TaskQueue priority_queue_;
// 二级任务队列
TaskQueue second_queue_;
};
int main(){
Scheduler scheduler;
Task task;
scheduler.AddTask(task,true); // 左值
scheduler.AddTask(Task(),true); // 右值
scheduler.AddTask(std::move(task),true); // 右值
}
输出:
default constract
Scheduler this is copy
TaskQueue this is copy
default constract
Scheduler this is move
TaskQueue this is copy
Scheduler this is move
TaskQueue this is copy
结果:Scheduler能正确的调用左值和右值引用函数,但是TaskQueue中却都调用了左值引用的函数,右值引用无法传递下去。
原因:在Scheduler中AddTask函数的task参数都已经是具名对象了,相当于都是左值。
如何实现右值继续转发给右值?
改一下代码:
void AddTask(Task &&task,bool is_prioprity){
printf("Scheduler this is move\n");
if(is_prioprity) priority_queue_.AddTask(std::move(task));
else second_queue_.AddTask(std::move(task));
}
这样就ok了。
万能引用
但实际Scheduler中只是负责数据转发,并不处理实际的业务,也不关心Task是左值引用还是右值引用,写两个AddTask是不是麻烦了一点,有没有更好的办法?有的,万能引用:
// 调度器
class Scheduler{
public:
// 通过引用折叠实现了万能引用
template<typename T>
void AddTask(T&& task,bool is_prioprity){
if(is_prioprity) priority_queue_.AddTask(task);
else second_queue_.AddTask(task);
}
private:
// 优先任务队列
TaskQueue priority_queue_;
// 二级任务队列
TaskQueue second_queue_;
};
引用折叠:在模板函数中实参的引用和形参的引用会形成引用折叠,所以可以使用模板函数实现万能引用。
但还是出现之前的问题,task都变成了具名对象。怎么办?std::forward 完美转发。
完美转发
// 调度器
class Scheduler{
public:
template<typename T>
void AddTask(T &&task,bool is_prioprity){
if(is_prioprity) priority_queue_.AddTask(std::forward<T>(task));
else second_queue_.AddTask(std::forward<T>(task));
}
private:
// 优先任务队列
TaskQueue priority_queue_;
// 二级任务队列
TaskQueue second_queue_;
};
很完美了。
全部代码:
#include <stdio.h>
#include <utility>
// 任务
class Task{
public:
Task(){
printf("default constract\n");
}
Task(const Task&){
printf("copy constract\n");
}
Task(Task&&){
printf("move constract");
}
};
// 任务队列
class TaskQueue{
public:
void AddTask(Task &){
printf("TaskQueue this is copy\n");
}
void AddTask(Task&&){
printf("TaskQueue this is move\n");
}
};
// 调度器
class Scheduler{
public:
template<typename T> // 为了防止出错,可以对T添加traits约束
void AddTask(T &&task,bool is_prioprity){
if(is_prioprity) priority_queue_.AddTask(std::forward<T>(task));
else second_queue_.AddTask(std::forward<T>(task));
}
private:
// 优先任务队列
TaskQueue priority_queue_;
// 二级任务队列
TaskQueue second_queue_;
};
int main(){
Scheduler scheduler;
Task task;
scheduler.AddTask(task,true); // 左值
scheduler.AddTask(Task(),true); // 右值
scheduler.AddTask(std::move(task),false); // 右值
}
这样就可以实现左值转发给左值,右值转发到右值。
2.3.2 旧文档
在函数模板中,完全依照模板的参数的类型(即保持参数的左值、右值特征),将参数传递给函数模板中调用的另外一个函数。完美转发需要用到std::forward函数。
完美转发是指将右值引用实际引用的值类型进行转发,左值转发到左值引用的函数,右值转发到右值引用的函数。可以发现移动构造也是通过完美转发实现的。
- 左值被转发为左值,右值被转发为右值
void show_log(string& str) { cout << "string & show " << str.data() << endl; }
void show_log(string &&str) { cout << "string && show " << str.data() << endl; }
// 参数转发
template <typename T>
void show(T&& arg)
{
// show_log(arg); // arg为具名变量,为左值,只有左值函数会被调用
show_log(std::forward<T>(arg)); // std::forward获取变量的原类型
}
int main()
{
string msg = "hello";
show(msg);
show(std::move(msg));
}
void foo(myVector& v) {}
// 参数转发
template<typename T>
void relay(T arg) {
foo(arg);
}
int main() {
myVector reusable= reateMyVector();
// 拷贝构造函数
relay(reusable);
// 移动构造函数
relay(createMyVector());
}
这个实现有个问题,假如定义了两个版本的 foo
void foo(myVector& v) {}
void foo(myVector&& v) {}
永远只有 foo(myVector& v) 会被调用 (右值引用myVector&& v是个左值,这里的v是有名字的)
所以,我们需要改写上文的 relay 函数,借助 std::forward
template<typename T>
void relay(T&& arg) {
foo(std::forward<T>(arg));
}
于是就有
- relay(reusable) 调用 foo(myVector&)
- relay(createMyVector()) 调用 foo(myVector&&)
要解释完美转发的原理,首先引入 C++11 的引用折叠原则
- T& & => T&
- T& && => T&
- T&& & => T&
- T&& && => T&&
所以,在 relay 函数中
- relay(9); => T = int&& => T&& = int&& && = int&&
- relay(x); => T = int& => T&& = int& && = int &
因此,在这种情况下,T&& 被称作 universal reference,即满足
- T 是模板类型
- T 是被推导出来的,适用引用折叠,即 T 是一个函数模板类型,而不是类模板类型
然后,C++11 定义了 remove_reference,用于返回引用指向的类型
template<class T>
struct remove_reference;
remove_reference<int&>::type == int
remove_reference<int>::type == int
于是,std::forword 的实现如下
template<class T>
T&& forward(
typename remove_reference<T>::type& arg
) {
return static_cast<T&&>(arg);
}
等于是把右值引用(右值引用本身是个左值)转成了右值,左值保持不变
3. 小结
std::move 对比 std::forward:
- std::move(arg) 将 arg 转化为右值
- std::forward(arg) 将 arg 转化为 T&&