so true

心怀未来,开创未来!
随笔 - 156, 文章 - 0, 评论 - 40, 引用 - 0
数据加载中……

深度剖析右值、右值引用、完美转发等相关概念

右值、右值引用(T&&)、std::move、完美转发(std::forward)、通用引用(universal reference)、引用折叠(reference collapsing)这些概念貌似很玄,但其实很它们想做的事情很简单而且很单一,抓住本质就豁然开朗了。
上面这些概念的引入完全是为了解决一个问题:尽可能用浅拷贝代替深拷贝;具体该如何实施呢:
1. 定义move constructor或move operator=,如果你新定义的类没有move constructor或move operator=,那根本就用不上上面那些高级的概念;
2. 告诉编译器什么时候用move constructor(而不是用copy constructor),这就需要借助std::move来把左值转成右值;
下面是一些零散的箴言:
左值:能用&来取地址的东西(有名字的都是左值,不管其是否是const的,而且引用都是左值,因为引用肯定得起个名字去引用另外一个东西);
右值:不能用&来取地址的东西(说白了就是没名字的东西,例如函数的返回值);
const T&是万能引用,即 const T& t = XXX; //XXX是左值、右值都可以;
T& t = XXX; //XXX必须是左值;
T&& t = XXX; //XXX必须是右值;
const T&& t = XXX; //XXX必须是右值;
不要把T&&和const T&划等号,两个都能hold住临时变量,给临时变量续命,但:
T&& t1 = get_val();
const T& t2 = get_val();
t1和t2都是左值,因为都可以用&来取地址,但t1是非const的,因此可以t1.xxx = yyy;去修改t1里的内容(当然如果你定义const T&& t1 = get_val(),那就不能更改了),这样一来,下面这种代码受益了:
    string name = get_name(); //因为后面要修改name,所以不能用const string& name
    trim(name);
    可以修改为:
    string&& name = get_name();
    trim(name);
    
关于右值引用的生命周期(续命能续多久,下面讨论的也适用于const T&):
1. 函数不能返回对局部变量的引用(右值引用也不行),这是铁的原则;
2. 引用一旦建立(用一个名字hold住了临时变量),例如T&& t = get_val();那么这个临时变量的析构时机就是变量t退出其作用域的时候,因此下面的代码是错误的:
    struct A {
        A(T&& t): _t(t) {};
        T&& _t;
    };
    A* pa = NULL;
    {
        T&& t = get_val();
        pa = new A(t);
    }
    pa->_t; //runtime crash, _t已经析构了
    
std::move只做一件事:把左值转成右值,因此T t(std::move(get_val()));不管什么情况下都能保证调用move constructor来构造t;
c++0x里stl的各种容器都已经新增了move constructor,因此当你希望使用浅拷贝的时候可以借助std::move来达成所愿了,至于什么时候可以用浅拷贝,分两种情况,有些情况编译器会默认帮你去调用move constructor,但在你自己新定义的函数或者类成员方法里就需要你自己来判断了,大的原则就是:这个变量只是传递过去就好,中间可以转好几道手,但在传递过程中不需要做修改;
完美转发(std::forward),谈到这个就必须得谈到通用引用(universal reference)、引用折叠(reference collapsing),其实这几个概念是捆绑在一起的,而且只用在模板范畴内,而且只是为了解决下面这一种模式:
template <typename T>
void func2(T t) {
}
template <typename T>
void func(T&& t) {
    func2(std::forward<T>(t));
}
上面这个到底完美在哪里?
func(get_val()); //这个会导致最后是通过move constructor来构造func2函数里的参数t
T t0 = get_val();
T& t1 = t0;
const T& t2 = get_val();
T&& t3 = get_val();
const T&& t4 = get_val();
func(t0); func(t1); func(t2); func(t3); func(t4); //这5个都会导致最后是通过copy constructor来构造func2函数里的参数t,因为这个5个t*都是左值(有名字的就是左值);
    插播两个你有可能费解的地方:
    1. func函数的参数是T&&, t0、t1都是左值,不是说不能用左值赋值给右值吗?这就要提到引用折叠了(详情可参见最后我推荐的文章),说白了因为func是个模板函数才能这么干;
    2. t3类型是T&&,func的参数也是T&&,把t3传递过去,居然还是调用copy contructor,因为t3是右值引用,它引用了一个右值,但它本身却是左值,到了func函数内部,func函数只能知道它是个左值,了解不到它原本的面貌居然是个右值引用;如果还不理解,再看下下面:
    template <typename T>
    void print(T t) {
    }
    int x1 = 1; int& x2 = x1; const int& x3 = x1; int* x4 = &x1;
    分别用x{1..4}来调用print方法,你肯定知道x1、x2、x3到了print函数里,T就是int,只有x4会被print函数认为T是int*,引用这个概念在模板类型推导时是无法让模板参数感知到的;此外print(T)和print(T&)是不能同定义的,编译器会抱怨ambiguous,这个事实也能帮你多一些理解。
如果想让上面的这5个最终能通过move constructor来构造func2函数里的参数t,那么只要给每个都用func(std::move(t*))包装下就可以了;
再澄清下,func2函数的参数是类型T,并不是引用,因此无论如何都需要生成T的一个新实例,区别就是到底是通过copy constructor还是move constructor来生成了;
所以,所谓的完美就是体现在了forward能把本来是左值的按左值来传递,本来是右值的按照右值来传递,具体来说就是作为中间环节的func函数内部实现过程中使用了func2(std::forward<T>(t));这句话,使得可以按照调用方实际的真实情况告知给func2函数如何构造参数t,这有什么好处呢,还是那句话:在适当的时机做合适的引导,让编译器帮你调用浅拷贝。
除此之外,你完全可以忘掉这些玄乎其神的概念,所以只要会套用就可以了。
以上是一些梗概性或结论性的东西,再结合下面这篇文章,把骨头之外的血肉补上吧:
https://codinfox.github.io/dev/2014/06/03/move-semantic-perfect-forward/

posted on 2018-12-06 11:38 so true 阅读(13) 评论(0)  编辑  收藏 所属分类: C&C++


只有注册用户登录后才能发表评论。


网站导航: