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就不允许这样做了