条款23 理解std::movestd::forward

形参永远是左值,即便它的类型是一个右值引用:

  1. void f(Widget&& w);

w是左值。 因为是一个拷贝或者移动的目的地。(个人理解)

对于能否判断一个表达式是否是左值:取地址。如果能就是左值;不能就是右值。


std::movestd::forward在运行时不做任何事情。它们不产生任何可执行代码。它们实质上是转换函数(cast)(函数模板)。**std::move**无条件将它的实参转换为右值std::forward只在特定情况满足时进行转换。

所以std::move的一个更合适的名字可能是rvalue_cast之类的。因为它本身就是一个转型操作

  1. template<typename T> //在std命名空间
  2. typename remove_reference<T>::type&&
  3. move(T&& param)
  4. {
  5. using ReturnType = //别名声明,见条款9
  6. typename remove_reference<T>::type&&;
  7. return static_cast<ReturnType>(param);
  8. }

如果使用C++14,那么std::move就可以更加简洁地写:

  1. template<typename T> //在std命名空间
  2. decltype(auto) move(T&& param)
  3. {
  4. using ReturnType = //别名声明,见条款9
  5. typename remove_reference_t<T>&&;
  6. return static_cast<ReturnType>(param);
  7. }

对一个对象使用std::move就是告诉编译器,这个对象很适合被移动


移动构造函数只接收一个 non-const 的右值引用;拷贝构造函数的形参:lvalue-reference-to-const ( const 左值引用),允许被绑定到一个 const 右值上。所以如果传入 const type 的右值,可能会产生拷贝构造函数的调用、

所以哪怕用了**std::move**也有可能不是使用移动,而是拷贝


  • 要想移动某个对象,就不要声明它为常量 const,因为针对 const 对象的移动操作会直接变成复制操作
  • std::move实际上不移动任何对象,甚至不保证强制类型转换过的对象具备可移动能力。唯一可以确定,该结果是个右值。

  1. void process(const Widget& lvalArg); //处理左值
  2. void process(Widget&& rvalArg); //处理右值
  3. template<typename T> //用以转发param到process的模板
  4. void logAndProcess(T&& param) //万能引用
  5. {
  6. auto now = //获取现在时间
  7. std::chrono::system_clock::now();
  8. makeLogEntry("Calling 'process'", now);
  9. process(std::forward<T>(param));
  10. }

当且仅当传给**param**的实参是一个右值的时候,才把param强制转换为右值类型。而且std::forward的实参,必须是一个非引用类型,比如:单纯的左值或者右值。

两次调用的结果:

  1. Widget w;
  2. logAndProcess(w); //用左值调用
  3. logAndProcess(std::move(w)); //用右值调用

image.png

条款24 区分万能引用和右值引用

T&&可以是绑定到右值引用,也可以绑定到左值引用。(万能引用)

以下两种情况会出现通用引用

  1. 函数模板形参

    1. template<typename T>
    2. void f(T&& param); //param是一个通用引用
  2. auto 声明符

    1. auto&& var2 = var1; //var2是一个通用引用

这两种情况都需要类型推导

如果看到some_type&&但是不涉及类型推导,那么就是右值引用

  1. void f(Widget&& param) //此处param是一个右值引用

因为万能引用是引用,所以必须被初始化初始值决定了它是右值引用还是左值引用

  1. template<typename T>
  2. void f(T&& param); //param是一个万能引用
  3. Widget w;
  4. f(w); //传递给函数f一个左值;param的类型
  5. //将会是Widget&,也即左值引用
  6. f(std::move(w)); //传递给f一个右值;param的类型会是
  7. //Widget&&,即右值引用

只有声明为T&&时才是万能引用,其他都不是。哪怕是一个const修饰符,也会让它失去万能引用资格

  1. template<typename T>
  2. void f(const T&& param); //param是一个右值引用

std::vector中,成员函数push_back不涉及类型推导,但是emplace_back却涉及类型推导:

  1. template<tyepname T,class Allocator = allocator<T>>
  2. class vector{
  3. public:
  4. void push_back(T&& x);
  5. template<class... Args>
  6. void emplace_back(Arg&&...args);
  7. };

其中,push_backstd::vector被实例化后,也跟着实例化了,于是它的T&&就变成了type&&;而**emplace_back**的形参**Args**独立于**T**。所以Args必须在每次被调用时进行推导。

条款25 对右值引用使用std::move,对万能引用使用std::forward

转发右值引用对象给其他函数时,应当对其实施向右值无条件的强制转换(通过std::move),因为它们一定绑定到右值;而当转发万能引用时,应当有条件的强制转换(通过std::forward),因为它们不一定绑定到右值:

  1. class Widget{
  2. public:
  3. //右值引用的处理操作:
  4. Widget(Widget&& w):name(std::move(w.name)){}
  5. //万能引用的处理操作:
  6. template<typename T>
  7. void setName(T&& newName){
  8. name = std::forward<T>(newName);
  9. }
  10. private:
  11. std::string name;
  12. };

如果不使用万能引用版本的setName(),使用如下版本:

  1. void setName(std::string&& newName){
  2. name = std::move(newName);
  3. }

该版本会导致一个临时**std::string**对象被创建,以供其形参绑定,然后这个临时对象移动到该 class 的数据成员中。所以总成本为:一次构造函数,一次移动赋值,一次析构。

而万能引用版本,字面字符串“blah blah”可以被传递、直接转发setName(),而不用额外临时对象创建的成本。

所以分别针对左值、右值形参重载,以期替换万能引用形参的方式,不仅会造成代码过多,还会造成运行期效率过低


对要拷贝返回值的右值引用形参使用std::move,会将拷贝构造变为移动构造

  1. Matrix operator+(Matrix&& lhs, const Matrix& rhs){
  2. lhs+=rhs;
  3. return std::move(lhs);
  4. }

但是对于局部变量 不可以!

编译器会在按值返回的函数中消除对局部对象的拷贝(或移动),如果满足:

  1. 局部对象与函数返回值的类型相同
  2. 局部对象就是要返回的东西

编译器会使用返回值优化(RVO),通过在分配给函数返回值的内存中构造 w 来避免复制局部变量 w :

  1. Widget makeWidget(){
  2. Widget w;
  3. ...
  4. return w;
  5. }

如果犯贱,干了return std::move(w)的事,就不会发生RVO/NRVO,因为违背了上述第二条规则,使用了std:move的返回值实际上返回的是局部对象的一个引用,适得其反地限制了编译器优化

在g++中使用**-fno-elide-constructors**的编译选项来关闭RVO优化,可以更好的观察。

条款26 避免重载万能引用

  1. std::multiset<std::string> names; //全局数据结构
  2. void logAndAdd(const std::string& name)
  3. {
  4. auto now = //获取当前时间
  5. std::chrono::system_clock::now();
  6. log(now, "logAndAdd"); //志记信息
  7. names.emplace(name); //把name加到全局数据结构中;
  8. } //emplace的信息见条款42

考虑这三个调用:

  1. std::string petName("Darla");
  2. logAndAdd(petName); //传递左值std::string
  3. logAndAdd(std::string("Persephone")); //传递右值std::string
  4. logAndAdd("Patty Dog"); //传递字符串字面值

第二个调用是一个临时对象,所以是一个右值。
第三种调用形参name绑定一个右值,但是是通过”Patty Dog”隐式创建的临时std::string变量。由于**name**是一个左值,所以就像第二个调用中,传参是拷贝形式。而后,name被拷贝到names中。

在第三个调用中,传递给logAndAdd()的实参是一个字符串字面量,如果该字面量被直接传递给emplace,那么就完全没有必要构造一个std::string的临时对象,完全可以利用它直接在 multiset 中构造一个std::string对象。

可以通过重写logAndAdd()来提升效率:

  1. template<typename T>
  2. void logAndAdd(T&& name)
  3. {
  4. auto now = std::chrono::system_lock::now();
  5. log(now, "logAndAdd");
  6. names.emplace(std::forward<T>(name));
  7. }
  8. std::string petName("Darla"); //跟之前一样
  9. logAndAdd(petName); //跟之前一样,拷贝左值到multiset
  10. logAndAdd(std::string("Persephone")); //移动右值而不是拷贝它
  11. logAndAdd("Patty Dog"); //在multiset直接创建std::string
  12. //而不是拷贝一个临时std::string

形参为万能引用的函数是最贪婪的。它会在具现化过程中,和几乎任何实参类型都会产生精确匹配。

如果在万能引用上重载了,那么可能调用时不会调用你的重载函数,而是由 template 推导出的精确匹配的通用引用的那个函数。

  1. class Person {
  2. public:
  3. template<typename T> //完美转发的构造函数
  4. explicit Person(T&& n)
  5. : name(std::forward<T>(n)) {}
  6. explicit Person(int idx); //int的构造函数
  7. Person(const Person& rhs); //拷贝构造函数(编译器生成)
  8. Person(Person&& rhs); //移动构造函数(编译器生成)
  9. };
  10. Person p("Nancy");
  11. auto cloneOfP(p); //从p创建新Person;这通不过编译!

image.png
此处原因是用一个 non-const 对象构造ww,而不是一个 const 对象。
如果以一个 const 对象调用构造函数,那么就可以匹配到拷贝构造函数而不是完美转发的构造函数了:
image.png

image.png

条款27 熟悉万能引用重载的替代方法

有点神棍,看不懂…

条款28 理解引用折叠

  1. template<typename T>
  2. void func(T&& param);
  3. Widget widgetFactory(); //返回右值的函数
  4. Widget w; //一个变量(左值)
  5. func(w); //用左值调用func;T被推导为Widget&
  6. func(widgetFactory()); //用右值调用func;T被推导为Widget

当左值实参传入时,T被推导为左值引用;右值被传入时,被推导为非引用

然而,C++不允许引用的引用,那么T被推导为左值引用的话,param就变成了Widget& &&,错!

所以有了引用折叠,有四种可能的引用组合(左值的左值,右值的右值,右值的左值,左值的右值)

如果一个上下文中允许引用的引用存在(比如,模板的实例化),引用根据规则折叠为单个引用:

如果任一引用为左值引用,则结果为左值引用。只有 右值引用的右值引用 结果才为右值引用。


引用折叠是std::forward工作的关键机制。可以这样实现:

  1. template<typename T> //在std命名空间
  2. T&& forward(typename
  3. remove_reference<T>::type& param)
  4. {
  5. return static_cast<T&&>(param);
  6. }

C++14可以使用remove_reference_t<T>而且不用加typename了。


万能引用并不是一种新引用,它实际上是满足以下两个条件的右值引用:

  • 类型推导区分左值和右值。 T 类型左值被推导为 T&,T 类型右值被推导为 T
  • 发生引用折叠。

条款29 假定移动操作不存在、成本高、未使用

C++只会在没有声明拷贝操作,移动操作,和析构函数的类中才会自动生成移动操作


std::array是C++11的新容器。std::array本质上是具有 STL 接口的内置数组,其他标准容器将内容存储在堆内存中本身只保存了指向堆内存中容器内容的指针。这一点,std::array与其他容器不同。 这个指针的存在使得在常数时间移动整个容器成为可能。只需要从源容器拷贝保存指向容器内容的指针到目标容器,然后将源指针置为空指针就可以了。

  1. std::vector<Widget> vm1;
  2. //把数据存进vm1
  3. //把vm1移动到vm2。以常数时间运行。只有vm1和vm2中的指针被改变
  4. auto vm2 = std::move(vm1);

image.png

std::array没有这种指针实现,数据就保存在该对象中:

  1. std::array<Widget, 10000> aw1;
  2. //把数据存进aw1
  3. //把aw1移动到aw2。以线性时间运行。aw1中所有元素被移动到aw2
  4. auto aw2 = std::move(aw1);

image.png


另一方面,std::string提供了常数时间的移动操作和线性时间的复制操作。这听起来移动比复制快多了,但是可能不一定。许多字符串的实现采用了小字符串优化small string optimization,SSO)。“小”字符串(比如长度小于15个字符的)存储在了**std::string**的缓冲区中并没有存储在堆内存,移动这种存储的字符串并不比复制操作更快。

Hint:今天才看到 Cherno 的一条弹幕说的 SSO 。


以下几种情况下,C++11的移动语义无优势

  • 没有移动操作:要移动的对象没有提供移动操作,所以移动会变成复制
  • 移动不会更快:要移动的对象提供的移动操作不比复制更快;
  • 移动不可用:某些容器提供了强大的异常安全保证,所以要求移动操作声明为noexcept,否则编译器可能强迫使用复制操作;
  • 源对象是左值:除了极少数情况外,只有右值可以作为移动操作的来源。

条款30 熟悉完美转发失败的情况

对于通常的转发来说,我们希望处理引用形参,而不是值传递形参和指针形参

完美转发模板

  1. template<typename T>
  2. void fwd(T&& param) //接受任何实参
  3. {
  4. f(std::forward<T>(param)); //转发给f()
  5. }

如果f()使用某特定实参会执行某个操作,但是fwd()使用相同的实参会执行不同的操作,完美转发就会失败。

花括号初始化器

假设f()这样定义:

  1. void f(const std::vector<int>& v);
  2. f({ 1, 2, 3 }); //可以,“{1, 2, 3}”隐式转换为std::vector<int>
  3. 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数据成员提供一个定义:

  1. const std::size_t Widget::MinVals; //在Widget的.cpp文件

参考网页

image.png

image.png

此处需要的情况是第一个Session 5: 右值引用、移动语义和完美转发 - 图9

重载函数的名称和模板名称

  1. void f(int (*pf)(int)); //pf = “process function”
  2. int processVal(int value);
  3. int processVal(int value, int priority);
  4. f(processVal); //可以
  5. fwd(processVal); //错误!哪个processVal?

可以创造一个与f()相同形参类型的函数指针,然后引导选择正确的版本:

  1. using ProcessFuncType = //写个类型定义;见条款9
  2. int (*)(int);
  3. ProcessFuncType processValPtr = processVal; //指定所需的processVal签名
  4. fwd(processValPtr); //可以
  5. fwd(static_cast<ProcessFuncType>(workOnVal)); //也可以

位域

感觉不堪大用。