C++右值、移动、完美转发

94 阅读4分钟

标题: C++右值、移动、完美转发

作者: 边城量子(shihezichen@live.cn


本文通过例子讲解C++中的左值/右值概念、 移动概念、完美转发概念, 帮助理解移动构造函数、通用引用、完美转发。

移动构造函数

1. 在没有移动构造函数之前哪里有性能瓶颈?

int main(int argc, char *argv[]){
    std::vector<A> vec;
    vec.push_back(A());
}

在main函数中,定义了一个A类型的vector,然后用A类型创建一个对象,然后把它放入vector. 这段代码有较大的性能问题,因为在A对象放入到vector时,在vector内部又创建了一个对象,并调用其拷贝构造函数进行了深拷贝。如果在A对象中分配的是一个比较大的空间,且vector中要存放大量的A对象时(如 100000个),就会不断的做分配/释放堆空间的操作,这会造成很大的性能消耗。

2. 添加移动构造函数

class A {
    public:
        ...

        A(A&& a){
            std::cout << "A move construct ..." << std::endl;
            ptr_ = a.ptr_;
            a.ptr_ = nullptr;
        }
        ...
};

int main(int argc, char *argv[]){
    std::vector<A> vec;
    vec.push_back(std::move(A()));
}

移动构造函数参数是 A&& a,代表它需要一个右值参数,但是A()是一个左值,因此需要通过std::move(A()) 转成右值。vector内部通过移动构造函数创建A对象,减少了对堆空间的频繁操作。

左值和右值

概念

保存在CPU寄存器中的值为右值,而保存在内存中的值为左值。 左值(lvalue): locator value, 可存储在内存中,有明确存储地址(可寻址)的数据 右值(rvalue): read value, 可以提供数据值的数据(不一定可寻址, 比如存在寄存器中的数据)

程序运行时并不是直接从内存中取令运行的,因为内存相对于CPU来说太慢了。一般情况下都是先将一部分指令读到CPU的指令寄存器,CPU再从指令寄存器中取指令然后一条一条的执行。对于数据也是一样,先将数据从内存中读到数据寄存器,然后CPU从数据寄存器读数据。

一个常数5,是放在寄存器中,在C++中是一个右值。定义一个变量a,它在内存中会分配空间,在C++中是一个左值。a+5的结果是右值,放到寄存器中。

例子

int&& a = 5;    // 正确, 5直接存放在寄存器中,它是右值;
                // a是左值,但是特殊点在于它只能接受右值赋值
int b = 10;  
int&& c = b;    // 错误,b在内存中,是右值,不能赋值给左值引用
int&& d = b + 5;   // 正确,虽然b在内存中,但b+5的结果存放在寄存器中,是右值
int&& e = a;       // 错误,a虽然接受了右值,但它自己是左值,无法复制给e,e只能接受右值
int& f = 10;   // 错误, 10是右值, &表示左值引用, 无法为右值建立左值引用
int&& g = 10;  // 正确

通用引用

通用引用既可以接受左值,也可以接受右值。

例子:


int a = 123;
auto&& b = 5;     // 通用引用,可以接收右值
int&& c = a;      // 错误
auto&& d = a;     // 通用引用,可以接收左值
const auto&& e = a;    //错误, 加了const就不再是通用引用

template<typename T>
void f(T&& param){
    
}

完美转发

完美转发指的是函数模板可以将自己的参数"完美"地转发给内部调用的其他函数,所谓完美,即不仅能准确的转发参数的值,还能保证被转发参数的左、右值属性不变。

未实现完美转发的例子:

template <typename T>
void function(T t)
{
    orderdef(t);
}

对上述例子, 完美转发指的是: 如果function函数接收到的参数t无论为左值还是右值, 都原封不动的传递给otherdef(), 很显然,上述例子未实现完美转发, 参数t为非引用类型,因此调用function函数时,实参将值传递给形参的过程就要额外进行一次拷贝操作,然后保存在一个内部的参数t中,它永远为左值,因此orderdef的参数也永远为左值.

修改为如下形式:

template <typename T>
void function(T&& t) 
{
    otherdef(std::forward<T>(t));
}
  1. 此模板函数的参数t既可以接受左值,也可以接受右值.
  2. function内部形参原本既有名称又能寻址, 它是左值, 通过模板函数forward<T>() 修饰后就能保持参数的左/右值属性.

完美转发的原理:

template <typename T>
T&& forward(typename std::remove_reference<T>::type& param)
{
    return static_cast<T&&>(param);
}

template <typename T>
T&& forward(typename std::remove_reference<T>::type&& param)
{
    return static_cast<T&&>(param);
}