C++左值右值与右值引用

本文总结 C++ 左右值的定义,和 C++11 中右值引用的用途。

转载:C++11新特性3 - 左右值与右值引用
C++ 11: Rvalue Reference – Move Sematics
C++ 11: Rvalue Reference – Fowarding


C++11通过引入右值引用来优化性能,通过移动语义来减少无谓的拷贝,通过完美转发来解决不能按照参数实际类型来

要点:

  1. 什么是右值:字面值和临时值/将亡值称为右值,或者说不能取地址的值称为右值
  2. 右值引用特点一:引用函数返回的将亡值,延长将亡值得生命周期,减少对象拷贝次数(const常量引用也能实现同样的效果,编译器返回值优化RVO能实现更好的效果(g++ -fno-elide-constructors关闭优化))
  3. 右值引用特点二:既可引用右值,又可引用左值,
  4. 右值引用特点三: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));   
}

由于编译器调用时无法区分

  1. printInt(int) 与 printInt(int&)
  2. 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 函数,比较麻烦:

  1. foo_by_value(myVector)
  2. 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&&
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值