条款23 理解std::move
和std::forward
形参永远是左值,即便它的类型是一个右值引用:
void f(Widget&& w);
w是左值。 因为是一个拷贝或者移动的目的地。(个人理解)
对于能否判断一个表达式是否是左值:取地址。如果能就是左值;不能就是右值。
std::move
和std::forward
在运行时不做任何事情。它们不产生任何可执行代码。它们实质上是转换函数(cast)(函数模板)。**std::move**
无条件将它的实参转换为右值,std::forward
只在特定情况满足时进行转换。
所以std::move
的一个更合适的名字可能是rvalue_cast
之类的。因为它本身就是一个转型操作。
template<typename T> //在std命名空间
typename remove_reference<T>::type&&
move(T&& param)
{
using ReturnType = //别名声明,见条款9
typename remove_reference<T>::type&&;
return static_cast<ReturnType>(param);
}
如果使用C++14,那么std::move
就可以更加简洁地写:
template<typename T> //在std命名空间
decltype(auto) move(T&& param)
{
using ReturnType = //别名声明,见条款9
typename remove_reference_t<T>&&;
return static_cast<ReturnType>(param);
}
对一个对象使用std::move
就是告诉编译器,这个对象很适合被移动。
移动构造函数只接收一个 non-const 的右值引用;拷贝构造函数的形参:lvalue-reference-to-const ( const 左值引用),允许被绑定到一个 const 右值上。所以如果传入 const type 的右值,可能会产生拷贝构造函数的调用、
所以哪怕用了**std::move**
也有可能不是使用移动,而是拷贝。
- 要想移动某个对象,就不要声明它为常量 const,因为针对 const 对象的移动操作会直接变成复制操作。
std::move
实际上不移动任何对象,甚至不保证强制类型转换过的对象具备可移动能力。唯一可以确定,该结果是个右值。
void process(const Widget& lvalArg); //处理左值
void process(Widget&& rvalArg); //处理右值
template<typename T> //用以转发param到process的模板
void logAndProcess(T&& param) //万能引用
{
auto now = //获取现在时间
std::chrono::system_clock::now();
makeLogEntry("Calling 'process'", now);
process(std::forward<T>(param));
}
当且仅当传给**param**
的实参是一个右值的时候,才把param
强制转换为右值类型。而且std::forward
的实参,必须是一个非引用类型,比如:单纯的左值或者右值。
两次调用的结果:
Widget w;
logAndProcess(w); //用左值调用
logAndProcess(std::move(w)); //用右值调用
条款24 区分万能引用和右值引用
T&&
可以是绑定到右值引用,也可以绑定到左值引用。(万能引用)
以下两种情况会出现通用引用:
函数模板形参
template<typename T>
void f(T&& param); //param是一个通用引用
auto 声明符
auto&& var2 = var1; //var2是一个通用引用
这两种情况都需要类型推导。
如果看到some_type&&
但是不涉及类型推导,那么就是右值引用:
void f(Widget&& param) //此处param是一个右值引用
因为万能引用是引用,所以必须被初始化,初始值决定了它是右值引用还是左值引用。
template<typename T>
void f(T&& param); //param是一个万能引用
Widget w;
f(w); //传递给函数f一个左值;param的类型
//将会是Widget&,也即左值引用
f(std::move(w)); //传递给f一个右值;param的类型会是
//Widget&&,即右值引用
只有声明为T&&
时才是万能引用,其他都不是。哪怕是一个const修饰符,也会让它失去万能引用资格:
template<typename T>
void f(const T&& param); //param是一个右值引用
在std::vector
中,成员函数push_back
不涉及类型推导,但是emplace_back
却涉及类型推导:
template<tyepname T,class Allocator = allocator<T>>
class vector{
public:
void push_back(T&& x);
template<class... Args>
void emplace_back(Arg&&...args);
};
其中,push_back
在std::vector
被实例化后,也跟着实例化了,于是它的T&&
就变成了type&&
;而**emplace_back**
的形参**Args**
独立于**T**
。所以Args
必须在每次被调用时进行推导。
条款25 对右值引用使用std::move
,对万能引用使用std::forward
当转发右值引用对象给其他函数时,应当对其实施向右值无条件的强制转换(通过std::move
),因为它们一定绑定到右值;而当转发万能引用时,应当有条件的强制转换(通过std::forward
),因为它们不一定绑定到右值:
class Widget{
public:
//右值引用的处理操作:
Widget(Widget&& w):name(std::move(w.name)){}
//万能引用的处理操作:
template<typename T>
void setName(T&& newName){
name = std::forward<T>(newName);
}
private:
std::string name;
};
如果不使用万能引用版本的setName()
,使用如下版本:
void setName(std::string&& newName){
name = std::move(newName);
}
该版本会导致一个临时**std::string**
对象被创建,以供其形参绑定,然后这个临时对象移动到该 class 的数据成员中。所以总成本为:一次构造函数,一次移动赋值,一次析构。
而万能引用版本,字面字符串“blah blah”可以被传递、直接转发给setName()
,而不用额外临时对象创建的成本。
所以分别针对左值、右值形参重载,以期替换万能引用形参的方式,不仅会造成代码过多,还会造成运行期效率过低。
对要拷贝返回值的右值引用形参使用std::move
,会将拷贝构造变为移动构造:
Matrix operator+(Matrix&& lhs, const Matrix& rhs){
lhs+=rhs;
return std::move(lhs);
}
但是对于局部变量 不可以!
编译器会在按值返回的函数中消除对局部对象的拷贝(或移动),如果满足:
- 局部对象与函数返回值的类型相同。
- 局部对象就是要返回的东西。
编译器会使用返回值优化(RVO),通过在分配给函数返回值的内存中构造 w 来避免复制局部变量 w :
Widget makeWidget(){
Widget w;
...
return w;
}
如果犯贱,干了return std::move(w)
的事,就不会发生RVO/NRVO,因为违背了上述第二条规则,使用了std:move
的返回值实际上返回的是局部对象的一个引用,适得其反地限制了编译器优化。
在g++中使用**-fno-elide-constructors**
的编译选项来关闭RVO优化,可以更好的观察。
条款26 避免重载万能引用
std::multiset<std::string> names; //全局数据结构
void logAndAdd(const std::string& name)
{
auto now = //获取当前时间
std::chrono::system_clock::now();
log(now, "logAndAdd"); //志记信息
names.emplace(name); //把name加到全局数据结构中;
} //emplace的信息见条款42
考虑这三个调用:
std::string petName("Darla");
logAndAdd(petName); //传递左值std::string
logAndAdd(std::string("Persephone")); //传递右值std::string
logAndAdd("Patty Dog"); //传递字符串字面值
第二个调用是一个临时对象,所以是一个右值。
第三种调用形参name
绑定一个右值,但是是通过”Patty Dog”隐式创建的临时std::string
变量。由于**name**
是一个左值,所以就像第二个调用中,传参是拷贝形式。而后,name
被拷贝到names
中。
在第三个调用中,传递给logAndAdd()
的实参是一个字符串字面量,如果该字面量被直接传递给emplace
,那么就完全没有必要构造一个std::string
的临时对象,完全可以利用它直接在 multiset 中构造一个std::string
对象。
可以通过重写logAndAdd()
来提升效率:
template<typename T>
void logAndAdd(T&& name)
{
auto now = std::chrono::system_lock::now();
log(now, "logAndAdd");
names.emplace(std::forward<T>(name));
}
std::string petName("Darla"); //跟之前一样
logAndAdd(petName); //跟之前一样,拷贝左值到multiset
logAndAdd(std::string("Persephone")); //移动右值而不是拷贝它
logAndAdd("Patty Dog"); //在multiset直接创建std::string
//而不是拷贝一个临时std::string
形参为万能引用的函数是最贪婪的。它会在具现化过程中,和几乎任何实参类型都会产生精确匹配。
如果在万能引用上重载了,那么可能调用时不会调用你的重载函数,而是由 template 推导出的精确匹配的通用引用的那个函数。
class Person {
public:
template<typename T> //完美转发的构造函数
explicit Person(T&& n)
: name(std::forward<T>(n)) {}
explicit Person(int idx); //int的构造函数
Person(const Person& rhs); //拷贝构造函数(编译器生成)
Person(Person&& rhs); //移动构造函数(编译器生成)
…
};
Person p("Nancy");
auto cloneOfP(p); //从p创建新Person;这通不过编译!
此处原因是用一个 non-const 对象构造ww
,而不是一个 const 对象。
如果以一个 const 对象调用构造函数,那么就可以匹配到拷贝构造函数而不是完美转发的构造函数了:
条款27 熟悉万能引用重载的替代方法
有点神棍,看不懂…
条款28 理解引用折叠
template<typename T>
void func(T&& param);
Widget widgetFactory(); //返回右值的函数
Widget w; //一个变量(左值)
func(w); //用左值调用func;T被推导为Widget&
func(widgetFactory()); //用右值调用func;T被推导为Widget
当左值实参传入时,T
被推导为左值引用;右值被传入时,被推导为非引用。
然而,C++不允许引用的引用,那么T
被推导为左值引用的话,param
就变成了Widget& &&
,错!
所以有了引用折叠,有四种可能的引用组合(左值的左值,右值的右值,右值的左值,左值的右值)
如果一个上下文中允许引用的引用存在(比如,模板的实例化),引用根据规则折叠为单个引用:
如果任一引用为左值引用,则结果为左值引用。只有 右值引用的右值引用 结果才为右值引用。
引用折叠是std::forward
工作的关键机制。可以这样实现:
template<typename T> //在std命名空间
T&& forward(typename
remove_reference<T>::type& param)
{
return static_cast<T&&>(param);
}
C++14可以使用remove_reference_t<T>
而且不用加typename
了。
万能引用并不是一种新引用,它实际上是满足以下两个条件的右值引用:
- 类型推导区分左值和右值。 T 类型左值被推导为 T&,T 类型右值被推导为 T
- 发生引用折叠。
条款29 假定移动操作不存在、成本高、未使用
C++只会在没有声明拷贝操作,移动操作,和析构函数的类中才会自动生成移动操作。
std::array
是C++11的新容器。std::array
本质上是具有 STL 接口的内置数组,其他标准容器将内容存储在堆内存中,本身只保存了指向堆内存中容器内容的指针。这一点,std::array
与其他容器不同。 这个指针的存在使得在常数时间移动整个容器成为可能。只需要从源容器拷贝保存指向容器内容的指针到目标容器,然后将源指针置为空指针就可以了。
std::vector<Widget> vm1;
//把数据存进vm1
…
//把vm1移动到vm2。以常数时间运行。只有vm1和vm2中的指针被改变
auto vm2 = std::move(vm1);
std::array
没有这种指针实现,数据就保存在该对象中:
std::array<Widget, 10000> aw1;
//把数据存进aw1
…
//把aw1移动到aw2。以线性时间运行。aw1中所有元素被移动到aw2
auto aw2 = std::move(aw1);
另一方面,std::string
提供了常数时间的移动操作和线性时间的复制操作。这听起来移动比复制快多了,但是可能不一定。许多字符串的实现采用了小字符串优化(small string optimization,SSO)。“小”字符串(比如长度小于15个字符的)存储在了**std::string**
的缓冲区中,并没有存储在堆内存,移动这种存储的字符串并不比复制操作更快。
Hint:今天才看到 Cherno 的一条弹幕说的 SSO 。
以下几种情况下,C++11的移动语义无优势:
- 没有移动操作:要移动的对象没有提供移动操作,所以移动会变成复制;
- 移动不会更快:要移动的对象提供的移动操作不比复制更快;
- 移动不可用:某些容器提供了强大的异常安全保证,所以要求移动操作声明为
noexcept
,否则编译器可能强迫使用复制操作; - 源对象是左值:除了极少数情况外,只有右值可以作为移动操作的来源。
条款30 熟悉完美转发失败的情况
对于通常的转发来说,我们希望处理引用形参,而不是值传递形参和指针形参。
完美转发模板:
template<typename T>
void fwd(T&& param) //接受任何实参
{
f(std::forward<T>(param)); //转发给f()
}
如果f()
使用某特定实参会执行某个操作,但是fwd()
使用相同的实参会执行不同的操作,完美转发就会失败。
花括号初始化器
假设f()
这样定义:
void f(const std::vector<int>& v);
f({ 1, 2, 3 }); //可以,“{1, 2, 3}”隐式转换为std::vector<int>
fwd({ 1, 2, 3 }); //错误!不能编译
在对f()
的直接调用上,编译器会观察传入的实参并和形参比较,看看是否匹配,必要时执行隐式转换使得调用成功。此处会生成一个临时std::vector<int>
对象。
当通过函数模板fwd()
间接调用f()
时,编译器不再把传入给fwd()
的实参和f()
声明中的形参类型作比较,而是推导传入给fwd()
的实参类型。此处由于模板的规则:模板类型推导无法推导出列表初始化,所以直接无法编译了。(回忆:列表初始化是auto
才能推导出来的)
0或者NULL作为空指针
Item8说明当你试图传递0
或者NULL
作为空指针给模板时,类型推导会出错,会把传来的实参推导为一个整型类型(典型情况为int
)而不是指针类型。
结果就是不管是0
还是NULL
都不能作为空指针被完美转发。解决方法非常简单,传一个nullptr
而不是0
或者NULL
。
仅有声明的整形**static const**
数据成员
在二进制底层代码中,指针和引用是一样的。在这个水平上,引用是可以自动解引用的指针。
为了使用完美转发,需要为整形static const
数据成员提供一个定义:
const std::size_t Widget::MinVals; //在Widget的.cpp文件
此处需要的情况是第一个。
重载函数的名称和模板名称
void f(int (*pf)(int)); //pf = “process function”
int processVal(int value);
int processVal(int value, int priority);
f(processVal); //可以
fwd(processVal); //错误!哪个processVal?
可以创造一个与f()
相同形参类型的函数指针,然后引导选择正确的版本:
using ProcessFuncType = //写个类型定义;见条款9
int (*)(int);
ProcessFuncType processValPtr = processVal; //指定所需的processVal签名
fwd(processValPtr); //可以
fwd(static_cast<ProcessFuncType>(workOnVal)); //也可以
位域
感觉不堪大用。