ModernCpp-右值引用
            
               约 1908 字 
                 预计阅读 4 分钟 
         右值引用:延长临时变量的生命周期,解决将亡值的问题
认识右值引用
- 考虑如下一种情况,下面这样情况,由于x是一个临时变量,在代码块结束后,因为该变量生命周期结束,再使用其地址将会不安全,这里直接会出现segement fault,因为引用了错误的地址。
 
- 这里解决方法有两个,可以选择将x定义在堆上,或者直接返回值。定义在堆上,如果管理不慎会出现内存泄露的问题。返回返回值又会产生额外的开销。
 
1
2
3
4
5
6
  | 
int& func1() {
    // x 是左值
    int x = 100;
    return x;
}
  | 
 
 
1
2
3
4
5
6
7
8
  | 
int func2() {
    // x是左值
    int x = 100;
    return x;	
}
int&& y = func2();	// y会捕获x的将亡值,延长其生命周期
  | 
 
右值引用只可以引用右值,std::move()可以将左值转换为右值,从而延长临时变量的生命周期。左值会在代码段结束后变为将亡值。 
非常量左值引用只能引用左值,常量左值引用可以引用右值。 
- 需要注意的是,一个变量虽然引用的是右值,但这个变量仍是一个左值,它不是一个临时对象,也不是纯粹的字面量。
 
1
2
3
4
5
  | 
    // 注意这里a和b虽然都是引用的右值,但是他们是左值
    int&& a = 2;
    const int& b = 2;
	int& b = 2; // error
  | 
 
实例
- 两个string对象相加会产生临时的string对象。
 
- 这里
std::move()只是将左值转换为右值,并没有智能指针中转交所有权的意思,被转换的变量仍可以调用这块内存。 
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
  | 
int main() {
    std::string lv1 = "string";     // 左值
    std::string&& rv1 = std::move(lv1);   // 临时lv1转换为右值,之后lv1仍是一个左值。
    // lv1与rv1共享内存
    lv1[0] = 'a';
    std::cout << rv1 << '\n';
    std::cout << lv1 << '\n';
    std::string&& lv2 = lv1 + lv1;  // lv1 + lv1 是一个右值,lv2可以引用右值。
    const std::string& lv3 = lv1 + lv2;   // 常量左值引用可以引用右值
    return 0;    
}
output:
    atring
    atring
  | 
 
右值引用与移动构造函数
- 在C++11之前,对象存在构造函数主要有三种,默认构造函数,拷贝构造函数,赋值构造函数,但是这三种构造函数都不支持对临时值的引用。移动构造函数就是通过右值引用,使用临时对象构造新的对象,减少内存分配与释放的次数。
 
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
  | 
#include <iostream>
#include <vector>
#include <string.h>
class A {
    public:
        A(){
            std::cout << "A construct..." << std::endl;
            ptr_ = new int(100);
        }
        A(const A & a){
            std::cout << "A copy construct ..." << std::endl;
            ptr_ = new int();
            memcpy(ptr_, a.ptr_, sizeof(int));
        }
    
        A(A && a){
            std::cout << "A move construct ..." << std::endl;
            ptr_ = a.ptr_;
            // must set prev nullptr
            // prevent double free.
            a.ptr_ = nullptr;
        }
        ~A(){
            std::cout << "A deconstruct ..." << std::endl;
            if(ptr_){
                delete ptr_;
            }
        }
        A& operator=(const A & a) {
            std::cout << " A operator= ...." << std::endl;
            return *this;
        }
        int * getVal(){
            return ptr_;
        }
    private:
        int *ptr_;
};
int main(int argc, char *argv[]){
    std::vector<A> vec;
    vec.push_back(A());
}
  | 
 
右值引用的返回问题
标准
1
2
3
4
5
6
7
  | 
std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}
std::vector<int> rval_ref = return_vector(); // RVO/通过移动构造函数,构造一个新的对象。
  | 
 
- 不需要将返回值转换为右值,在这个函数结束时,它将通过编译器优化为一个右值。
 
- 不需要引用返回值。返回值首先会通过RVO进行优化,如果编译器不支持RVO,之后会调用移动构造函数,如果也不存在移动构造函数的话,则会调用拷贝构造函数构造该对象。
 
万能引用
- 万能引用(Universal Reference):可以接收左值也可以接收右值。
 
- 它存在两种使用条件:一种是模板,另外一种是auto(auto也是也是模板)。只有发生类型推导的时候,T&&才表示万能引用; 否则,表示右值引用。
 
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
  | 
template<typename T>
void f(T&& param){
    std::cout << "the value is "<< param << std::endl;
}
for (auto&& x: v) {
    // 如果要修改值
}
for (const auto& x: v) {
    // 如果不修改值
}
  | 
 
std::move
1
2
3
4
5
  | 
template <typename T>
typename remove_reference<T>::type&& move(T&& t) // 万能引用,可以引用左值,也能引用右值
{
    return static_case<typename remove_reference<T>::type&&>(t);
}
  | 
 
引用折叠
int & & 折叠为 int& 
int & && 折叠为 int& 
int && & 折叠为 int& 
int && && 折叠为 int && 
std::forward
- 进行参数多次引用的传递,std::forward可以将原来传递的参数是左引用还是右引用进行区分。
 
参考资料: