04区分通用引用和右值引用 Scott Meyers的effective modern c++讲座摘要

大师就是大师,可爱的蘑菇头,讲的太好了,很精彩!有激情,有耐心,很难解释的东西随手举几个例子就明白了!强烈推荐看看视频!
https://www.youtube.com/playlist?list=PLmxXlAVb5hkyq5njldMEPYdOqTAQPLChR

核心问题

  • 右值引用 一定是 type&&
  • type&& 不一定是 右值引用(还有可能是通用引用)

先来看看,type&&的双面性

void f(Widget&& param);        // 右值引用
Widget&& var1 = Widget();      // 右值引用

auto&& var2 = var1;            // 不是右值引用!!!

decltype(var1) var3 = var1;    // 无法编译通过
Widget&& var4 = var1;          // 无法编译通过

template<typename T>           
void f(std::vector<T>&& param); // 右值引用

template<typename T>
void f(T&& param);              // 不是右值引用!!!

在这里,"type&&"中的"&&"意味着:

  • 右值引用
     * 绑定右值(Binds rvalues only.)
     * 促进移动(Facilitates moves.)
  • 通用引用
    • 右值引用或左值引用(type&& 有可能是 type& 或 type&& )
    • 绑定所有值,不管是左值,右值,const, 非const...
    • 促进拷贝,也促进移动(May facilitate copies, may facilitate moves)
    • 与转发引用(Forwarding Reference)相同

如何区分,简单来说

  • 如果一个变量或参数的声明类型是T&&,并且需要推导出类型T, 这就是通用引用,否则就是右值引用
  • 通用引用是需要初始化的,如果是左值,那就是左值引用,如果是右值,那就是右值引用

下面主要通过示例来解释

template<typename T>
void f(T&& param)    // 典型的通用引用,T&& + 需要推导类型

Widget w;            
f(w);                // w是左值,通用引用就是左值引用,f(Widget&)

f(std::move(w));     // std::move无条件转换为右值,所以通用引用就是右值引用,f(Widget&&)
f(Widget());         // Widget()是右值,这也是右值引用,f(Widget&&)

std::vector<int> v;
...
auto&& val = 10;       // 10是右值,所以通用引用就是右值引用,val的类型就是int&&

auto&& element = v[5]; // v[5]返回的是int&, 所以这里是左值引用,element的类型是int&
// 有类型推导才能是通用引用
void f(Widget&& w);          // 没有类型推导,所以这里不是通用引用

template<typename T>
void f(T&& param);           // 有类型推导,所以是通用引用

template<typename T>
class Gadget1 {
    Gadget1(Gadget1&& rhs);  // 没有类型推导,不是通用引用
};

template<typename T1>
class Gadget2 {
    tempalte<typename T2>
    Gadget2(T2&& rhs);       // 有类型推导,是通用引用
};

// 但是不是所有的T&&都是通用引用
template<class T,
         class Allocator=allocator<T>>
class vector{
public:
    ...
    // 这里是右值引用!!! 因为T是从vecotr<T>来的,而不是通过push_back的参数传递来的
    // 我解释一下,这个T实际上是创建vector时就已经确定了,而不是等调用push_back时才做的类型推导
    void push_back(T&& x);    
    ...
};

std::vector<int> vi;
...
vi.push_back(10.5);    // 实参类型是double, 但形参类型是int&&

Scott在这里解释的比较多,就是push_back为什么要有一个通用引用的参数?

过去vector的push_back实际上会构造一个int, 然后放到vector中,这会导致效率问题,如果构造的是一个很大的对象,那么每次调用push_back时都会调用拷贝构造函数,那么就会导致效率直线下降,这也是造成过去标准库让人诟病的原因。

而现在引入右值引用,上面说过,是用于促进移动义的,因为10.5是一个右值,那么在放到vector中时就会直接移动这个右值,而不会再构造一个int!想像一下,如果是一个大对象,那么就会直接移动这个大对象,而不需要再调用拷贝构造函数,效率大大提升!

vector<BigObject> vi;

BigObject o;

vi.push_back(o);               // 这是还是会调用BigObject的拷贝构造函数

vi.push_back(std::move(0));    // std::move无条件转换为右值,这里不会再调用拷贝构造函数,而是直接移动
vi.push_back(BigObject());     // 这里也是右值,同上,不调用拷贝构造函数

最后比较一下push_back和emplace_back

从名称来看,push就是推到最后,有点不论怎么拿,各种暴力搬运,反正搁在最后就行了;而emplace就是放到最后,有点轻拿轻放的感觉。现在一般建议新代码直接使用emplace_back。

template<class T, class Allocator=allocator<T>>
class vector{
public:
    ...
    void push_back(const T& x);         // 左值引用,拷贝值
    void push_back(T&& x);              // 右值引用,移动值
    
    template<class... Args>
    void emplace_back(Args&&... args);  // 通用引用,转发所有值
    ...
}

现在实现中,两个push_back内部都是直接调用emplace_back, emplace_back是通用引用,所以可以接收任何值,无论左值还是右值。emplace_back使用变长参数,好处就是使用更加方便,比如:

class Widget
{
public:
    Widget(int a, int b);
};

std::vector<Widget> v;

Widget w;

v.emplace_back(w);               // 左值,拷贝值

v.emplace_back(std::move(w));    // 右值,移动值
v.emplace(Widget(10, 10));       // 右值,移动值
v.emplace(10, 10);               // 右值,移动值,这里使用了变长参数直接调用了构造函数,而push_back就不允许这样做了

posted @ 2019-03-29 11:03  hanjackcyw  阅读(685)  评论(0编辑  收藏  举报