C++右值引用与移动语义

550 阅读5分钟

之前一直在用右值引用最近看面经发现这个也是高频考点就在这里总结一下。主要内容是总结的 effective modern C++的第五章和部分自己的理解。

在讲右值引用之前我们首先要知道什么是左值和右值。首先先看一下官方的定义。

官方看起来很抽象,用我自己的语言来说就是有名字的变量也就是说有一个对象和这个值关联的值就叫左值否则就是右值。还有一个能赋值的就是左值不能赋值的就是右值。

右值引用存在的意义:

右值引用出现是为了在某些情况下对临时变量不必要的拷贝。举个例子

#include <iostream>
#include <string>

class Container {
 private:
  typedef std::string Resource;

 public:
  Container() {
    resource_ = new Resource;
    std::cout << "default constructor." << std::endl;
  }
  explicit Container(const Resource& resource) {
    resource_ = new Resource(resource);
    std::cout << "explicit constructor." << std::endl;
  }
  ~Container() {
    delete resource_;
    std::cout << "destructor" << std::endl;
  }
  Container(const Container& rhs) {
    resource_ = new Resource(*(rhs.resource_));
    std::cout << "copy constructor." << std::endl;
  }
  Container& operator=(const Container& rhs) {
    delete resource_;
    resource_ = new Resource(*(rhs.resource_));
    std::cout << "copy assignment." << std::endl;
    return *this;
  }

 private:
  Resource* resource_ = nullptr;
};

Container get() {
  Container ret("tag");
  return ret;
}

int main() {
  Container foo;
  // ...
  foo = get();
  return 0;
}
// $ ./a.out
// default constructor.
// explicit constructor.
// copy assignment.
// destructor
// destructor
会先构造一个临时对象再拷贝赋值

使用右值引用的注意点:

1. 顾名思义右值引用初始化的时候必须指向一个右值,但是常量左值引用可以指向右值

string&& r_value_ref1 = string("123"); //正确 指向的是右值
string& left_ref = string("456"); // 错误 非常量左值引用指向右值
const string& const_left_ref = string("789"); // 正确,常量左值引用指向右值
string&& r_value_ref2 = left_ref; // 错误,右值引用绑定的是左值

2. 希望将一个右值引用绑定到左值上可以调用std:move()方法,但是调用后之前的左值会发生所有权转移,因此不能再使用下面的图片抄自c++ primer

3. 对于const类型的变量move是无效的,他会悄悄的拷贝一份非const变量对象

那这个神秘的std::move() 又干了啥能让一个左值变成右值呢?看一下STL实现

现在看着是调用了一个remove_reference,然后把得到的值直接强制转换成右值引用。而这个remove_reference干的确实就是名字的事情,不管你传进来的是啥我最后返回一个没有引用类型的类型。

这么看来这个move并没有真的move,只是干了一件强转的微小工作。

通用引用

除了左值引用和右值引用还有一种更强大的叫做通用引用(universial reference),这个引用既可以指向左值也可以指向右值。具体要看指向的是什么。什么时候一个引用会视为通用引用呢?

如果一个函数模板参数的类型为T&&,并且T需要被推导得知,或者如果一个对象被声明为auto&&,这个参数或者对象就是一个通用引用。 -- 《effective modern C++》

更多见的是在模板中参数但是我们下面先拿auto&& 这种情况下的作参考。

#include <type_traits>
#include <iostream>
#include <string>
using namespace std;
int main(){    
    string item("test");    
    string& lvalue_ref = item;    
    string&& rvalue_ref = string("123");    
    auto&& auto_lvalue_ref = lvalue_ref;    
    auto&& auto_rvalue_ref1 = rvalue_ref;    
    auto&& auto_rvalue_ref2 = string("456");    
    cout << std::is_rvalue_reference_v<decltype(lvalue_ref)> << endl;    
    cout << std::is_rvalue_reference_v<decltype(rvalue_ref)> << endl;    
    cout << std::is_rvalue_reference_v<decltype(auto_lvalue_ref)> << endl;    
    cout << std::is_rvalue_reference_v<decltype(auto_rvalue_ref1)> << endl;    
    cout << std::is_rvalue_reference_v<decltype(auto_rvalue_ref2)> << endl;    
    return 0;
}
//输出
// 0
// 1
// 0
// 0
// 1

再看一个简单的模板的

#include <type_traits>
#include <iostream>
using namespace std;
template <typename T>
void universial_reference_test(T&& param){    
    if(std::is_rvalue_reference_v<decltype(std::forward<T>(param))>){        
        cout << "input is rvalue reference" << endl;    
    }else{        
        cout << "input is lvalue reference" << endl;    
    }
}
using namespace std;
int main(){
    int a = 1;
    int& lvalue_ref = a;
    int&& rvalue_ref = 2;
    universial_reference_test(lvalue_ref);
    universial_reference_test(rvalue_ref);   
    universial_reference_test(2);
    return 0;
}
// 输出
input is lvalue reference
input is lvalue reference
input is rvalue reference

可以看到auto&&类型的引用会根据不同的赋值类型有不同的引用。比较有意思的是给一个auto&& 类型的变量赋值一个一个右值引用,这个时候这个变量会被视为左值。虽然右值引用指向的是右值但是右值引用是一个左值!关于什么时候这个是通用引用还是右值引用,就是要看有没有发生推倒。如果没有发生推导则就是一个普通的右值引用。

我们再看一些不是通用引用仅仅是简单的右值引用的例子

template<typename T>
void rvalue_only_func(vector<T>&& v){    }

传入的参数并不是T&&类型,也没有发生推导所以就是一个普通的右值引用。

template<typename T>
void rvalue_only_func2(const T&& v){    }

带了const也不行,这样就是一个普通的右值引用。

更刁钻的是你看到了T&&的状态它还可能不是一个通用引用!下面的例子抄自effective modern C++中对STL库中vector的例子。

template <class T,class Allocator = allocator<T>> //来自C++标准
    class vector
    {
        public:
        void push_back(T&& x);
        ...
    }

push_back函数的参数当然有资格成为一个通用引用,然而,在这里并没有发生类型推导。 因为push_back在一个特有(particular)的vector实例化(instantiation)之前不可能存在,而实例化vector时的类型已经决定了push_back的声明。也就是说,

将会导致std::vector模板被实例化为以下代码:

std::vector<Widget> v;    
class vector<Widget , allocagor<Widget>>
    {
        public:
        void push_back(Widget&& x);             // 右值引用
    }

现在你可以清楚地看到,函数push_back不包含任何类型推导。push_back对于vector<T>而言(有两个函数——它被重载了)总是声明了一个类型为指向T的右值引用的参数。

相反,std::vector内部的概念上相似的成员函数emplace_back,却确实包含类型推导:

template <class T,class Allocator = allocator<T>> //依旧来自C++标准    
class vector
    {
        public:
        template <class... Args>
        void emplace_back(Args&&... args);
        ...
    }

这儿,类型参数(type parameter)Args是独立于vector的类型参数之外的,所以Args会在每次emplace_back被调用的时候被推导(Okay,Args实际上是一个参数包(parameter pack),而不是一个类型参数,但是为了讨论之利,我们可以把它当作是一个类型参数)。

虽然函数emplace_back的类型参数被命名为Args,但是它仍然是一个通用引用,这补充了我之前所说的,通用引用的格式必须是T&&。 没有任何规定必须使用名字T。举个例子,如下模板接受一个通用引用,但是格式(type&&)是正确的,并且参数param的类型将会被推导(重复一次,不考虑边缘情况,也即当调用者明确给定参数类型的时候)。

关于std::forward()的那些事

从上面的讨论我们可以看到在有通用引用的情况下,我们实现一个左值右值都可以用的接口。我们可以在接口内部再根据左值和右值采用不同的机制。但是我们怎么保留左值和右值的特性呢?这里急需要std::forward()了。先看一个简单的例子(直接抄的effective modern C++)。

先简单介绍一下这个emplace函数是个啥东西。由于引用了右值引用特性,就可以在某些情况下减少不必要的拷贝。比如如果传入的是右值就可以在容器内部原地构造,而左值就需要生成一份拷贝。所以在这个例子里第一项和第二项分别会产生拷贝和没有拷贝。就需要留下相关的左值右值信息。std::forward() 干的就是这样微小的工作。那我们再看看std::forward()具体干了啥事情

可以看到和move一样首先先调用remove_reference得到没有reference的变量type,然后再次强转成一个**通用引用(这里和move不一样了哦,虽然都是&&但是这里的_Tp&&和模板里的是一个type这是一个通用引用)。**可以看到forward和move一样只是做了一些微小的工作只是简简单单的强制类型转换罢了。

(待续)

参考资料

effective modern C++(强烈推荐看一下)

zh.cppreference.com/w/cpp/langu…

nettee.github.io/posts/2018/…

zhuanlan.zhihu.com/p/99524127