右值、左值、右值引用?

std::string a = "abc"; // a是一个泛左值glvalue, "abc"是纯右值prvalue
std::string& b = a; // b是一个泛左值,同时他是绑定到左值的左值引用

std::string make_a_string()
{
    std:string inside = "abc";
    return inside;
}
std::string c = make_a_string(); 
// 等式右侧是一个将亡值xvalue
// 返回的inside是一个即将被销毁的临时变量
// 这句话理论上发生的事情是
// 1. 函数make_a_string()中“构造”一个inside变量
// 2. inside作为函数的值返回,是一个xvalue,作为c的构造参数
//    即此处更明确(显式)的写法是
std::string c( make_a_string() );
//    而在出该行作用域前,这个构造函数是会执行类string的拷贝构造的
//    然而,一个将亡值,我们竟然在对其做拷贝构造,这不合理,我们应该直接窃取其资源
// 所以,函数make_a_string()理论上的返回语句应该写成
return std::move(inside)
// 或者在构造c时:
std::string c( std::move(make_a_string()) )
// 而实际上我们都不用这么做,因为c++11以后有了返回值优化的说法
// 编译器会帮我们尝试移动构造而不是拷贝构造,我们只需要实现对应对象的移动构造函数即可
std::string&& ra = std::move(a);

这句话表示ra本身值类别是一个左值,类型是一个右值引用,并没有新构造出一个对象。

std::string ra = std::move(a);

这句话表示使用移动构造函数构造出了一个新的对象ra;

std::move的典型应用

std::move函数实际上就是一个基于模板的强制转化为右值的方法,类似于(std::string&&)a,其相当于std::move(a)。

主要就是需要移动构造时,使用move将程序员自己临时创建的一个变量转化为右值,使得编译器调用类的实际移动构造函数。

一个精简的例子:

class Entity{
public:
    Entity(const std::string& a);
    Entity(const Entity& rhs); // copy
    Entity(Entity&& rhs); // move
private:
    std::string m_data;
};

int main()
{
    // 你通过某个函数获得了一个Entity类
    Entity entity1 = make_an_entity();
    // 然后你希望用这个entity1来构造另一个类对象,构造完之后这个entity1就不需要了
    Entity entity2 = std::move(entity1); // 调用了move构造函数
}

更详细的例子我觉得Cherno的移动语义部分那个例子更有意义,我这个只是一个看上去没什么意义的用法罢了。

完美转发

引用折叠

c++规定在使用模板时,可以使用一个称之为万能引用的模板函数参数类型,这归功于应用折叠:

template<typename T>
void do_something(T&& value) {
    do_it_actually_here(value);
}

万能引用就是,我可以给这个模板参数传递任何类型的值,左值或右值均可,如果传入的是一个字面量或一个右值,T将被推导为普通类型如string,如果传入的是一个左值,T将被推导为T&,通过引用折叠,前者得到的value是一个绑定到右值的右值引用,后者得到的value是一个绑定到左值的左值引用。

但是当我们需要对value进一步做一些什么的时候,比如我需要将value原封不动的让另一个函数来处理,那这里就会有问题。如当T被推导为string时,value是一个右值引用,但又因为value本身是一个左值变量,所以传递给do_it_actually_here()函数时,我们本意是传递一个右值给他去处理,如我们调用

std::string a = "hello";
do_something(std::move(a));

希望处理这么一个右值(将亡的),整个过程不希望发生拷贝构造,但事实上是因为value是一个左值,所以传递给do_it_actually_here(T&&)时,会被推导为一个左值,将会不可避免的发生一次从value到do_it_actually_here形参的拷贝构造,所以这里就需要完美转发。

将函数改为:

template<typename T>
void do_something(T&& value) {
    do_it_actually_here( std::forward<T>(value) );
}

forward函数的原理这里不做阐述,他做的就是,如果value是一个右值引用,根据传递给forward的模板参数类型如T=std::string,他将value强制转换为了一个右值,让调用的函数可以识别其是一个右值引用,调用移动语义相关的具体函数;如果value是个左值引用,那么他会根据T=std::string&,将value仍然保持为std::string&。

这个写法的精妙之处就在于,forward的模板参数T是调用do_something时就推导出的,所以也就解释了为什么forward能够获取原传递参数的类型信息。

感觉写的还是不太严谨,尤其涉及到右值引用的概念,实践中在看看有没有疏漏、错误和补充吧。