Item 23: Understand std::move and std::forward

先明确了解std::move 和 std::forward不会做什么很有必要。std::move不会移动任何东西,std::forward也不转发任何东西。在运行时,两者不做任何事情。它们不会生成任何可执行代码,甚至连一个字节都不会生成。

std::move 和 std::forward仅仅只是执行强制类型转换的函数(其实是函数模板)。std::move会无条件的将实参转换成右值,而std::forward仅在满足特定条件时才执行强制转换。这个解释引出了一系列的新问题,但从根本上说,这就是故事的全部。

为了使故事更加具体,下面是std::move 在c++ 11中的一个实现示例,虽然不完全符合标准的细节,但已经很接近了。

template<typename T>                             // in namespace std
typename remove_reference<T>::type&&
move(T&& param)
{
    using ReturnType =                           // alias declaration;
        typename remove_reference<T>::type&&;    // see Item 9
        
    return static_cast<ReturnType>(param);
}

分析一下上面的代码:std::move的形参是一个指向对象的引用(准确地说,是万能引用,Item 24),且返回的是指向同一个对象的引用。而函数返回值的“&&”部分,暗示着std::move返回的是个右值引用。 如Item 28所言,如果T碰巧是个左值引用的话,T&&也会变成左值引用。为了阻止这种情况发生,将std::remove_reference应用于T,从而确保“&&”应用在一个非引用类型之上。如此一来,就可以确保std::move返回的是右值引用,这点很重要,因为从该函数返回的右值引用肯定是右值。所以,std::move的作用就是将实参强制转换成了右值!

题外话,std::move在c++ 14中实现起来不那么麻烦。得益于函数返回类型推导(Item 3)和标准库别名模板std::remove_reference_t(Item 9),std::move可以这样写:

template<typename T>                   // C++14; still in
decltype(auto) move(T&& param)         // namespace std
{
    using ReturnType = remove_reference_t<T>&&;
    return static_cast<ReturnType>(param);
}

由于std::move只是做了强制类型转换,有人建议用类似rvalue_cast这样的名字更合适一些。但是,右值是可以被移动的,所以对一个对象应用std::move,就是告诉编译器这个对象有资格被移动。如此一来,以std::move命名,加暖了对象是否可移动的表述。

事实上,右值在通常情况下都可以被移动,但不是一定哦。也就是说,即便是右值,也不是一定会触发移动。 假设有一个Annotation类,这个类的构造函数接受一个std::string形参,并将该形参拷贝到一个成员变量中。 参阅Item 41,你可能写出一套按值传参的代码:

class Annotation {
public:
    explicit Annotation(std::string text);      // param to be copied,// so per Item 41,
};                                              // pass by value

但是Annotation的构造函数只会读取text的值,不需要修改它。你可能会修改成如下代码:

class Annotation {
public:
    explicit Annotation(const std::string text)};

为了避免拷贝text的拷贝开销,根据Item 41的建议,对text应用std::move,从而产生一个右值:

class Annotation {
public:
    explicit Annotation(const std::string text)
    : value(std::move(text))        // "move" text into value; this code
    {}                           // doesn't do what it seems to!private:
    std::string value;
};

成员变量value被设置为了text的值。但很遗憾,text不是被移动到value的,而是拷贝到value的。是的,text被std::move强转为一个右值,但是text被声明为一个const std::string,所以在强转之前,text是一个左值const std::string,强转的结果是一个右值const std::string,整个过程中,const属性被保留了下来。当编译器要抉择调用std::string的构造函数时,面临着两个选择:

class string {                    // std::string is actually a
public:                           // typedef for std::basic_string<char>string(const string& rhs);      // copy ctor
    string(string&& rhs);           // move ctor};  

在Annotation构造函数的成员初始化列表中,std::move(text)的结果是一个const std::string类型的右值。右值是没办法传给std::string的移动构造函数的,因为移动构造函数接受的是一个指向非const std::string的右值引用。可是,这样一个右值是可以传递给拷贝构造函数的,因为一个指向const的左值引用是允许绑定到一个常量右值的。 因此,成员初始化会调用std::string的拷贝构造函数,即使text已经被强转为一个右值!这种行为对于维持const正确性至关重要。从对象中移出值后通常会修改对象,因此编程语言不应允许将const对象传递给函数(例如move构造函数)来修改它们。

从这个例子中我们可以总结两点经验:
1,如果想取得对某个对象执行移动操作的能力,就不要将其声明为const, 因为针对针对常量对象执行的移动操作都不会被悄无声息的转换为其上的拷贝操作;
2,std::move不仅不实际移动任何东西,甚至不保证经过其强制转换后的对象具备可移动能力。
唯一可以确定的是,std::move的结果是一个右值。

std::forward的情况与std::move类似,但与std::move无条件地将其实参转换为右值不同,Std::forward是一个条件强制转换。为了理解它何时强制转换,何时不强制转换,请回忆一下std::forward的典型使用场景。其中最常见的,是某个函数模板接受一个万能引用形参,随后将其递给另一个函数:

void process(const Widget& lvalArg); // process lvalues
void process(Widget&& rvalArg);      // process rvalues

template<typename T>                 // template that passes
void logAndProcess(T&& param)        // param to process
{
    auto now =                       // get current time
        std::chrono::system_clock::now();
        
    makeLogEntry("Calling 'process'", now);
    process(std::forward<T>(param));
}

考虑对logAndProcess的两种调用情形,一个是左值,另一个是右值:

Widget w;

logAndProcess(w);                   // call with lvalue
logAndProcess(std::move(w));        // call with rvalue

在logAndProcess内部,param被传给了process函数。process函数有两个重载。当我们调用logAndProcess时,如果传递一个左值进去,自然期望这个左值也能作为左值传递给给process函数,而如果传递一个右值给logAndProcess时,我们又希望这个右值可以作为右值被传递给process函数。
但是,所有函数的形参都是左值,param也不例外! 那么logAndProcess内所有对process函数的调用,都会调用到process的左值重载版本上。为了阻止这种情况,我们需要一种机制,当且仅当用来初始化param的实参(即传递给logAndProcess的实参)是个右值的情况下,才把param强转为右值类型。这就是std::forward的用武之地,也是为何称std::forward是有条件的强制类型转换:当且仅当实参是使用右值完成初始化时,std::forward才会执行向右值类型的强制类型转换。

你可能会疑惑,std::forward是如何知道它的参数是否由右值完成初始化的呢。一句话:该信息是被编码到logAndProcess的模板参数T中的。该参数被传递给std::forward, 随即由std::forward恢复编码的信息。具体细节参见Item 28。

既然std::move 和 std::forward都是执行强制类型转换,唯一的不同,一个是无条件的,一个是有条件的,那么是否可以用std::forward代替std::move呢?从纯技术的角度来看,答案是可以的。但是,std::move的有点是方便、减少错误可能、更清晰。假设有一个类,我们想跟踪它的移动构造函数被调用的次数。类中还有一个非静态成员是std::string类型:

class Widget {
public:
    Widget(Widget&& rhs)
    : s(std::move(rhs.s))
    { ++moveCtorCalls; }private:
    static std::size_t moveCtorCalls;
    std::string s;
};

采用std::forward实现相同的行为,代码如下:

class Widget {
public:
    Widget(Widget&& rhs)                        // unconventional,
    : s(std::forward<std::string>(rhs.s))       // undesirable
    { ++moveCtorCalls; }                        // implementation};

注意,首先,std::move仅需要一个函数实参(rhs.s),而std::forward需要函数实参(rhs.s)和模板类型实参(std::string)。另外,传递给std::forward的实参类型应该是非引用类型,因为编码习惯上传递给它的实参应该是个右值(Item 28)。这么看来的话,std::move写的代码更少,也省去了需要传递一个类型实参为右值的麻烦。同事,std::move还消除了错误类型的可能行(比如std::string&,这会导致数据成员S被拷贝构造而非移动构造)。

更为重要的是,std::move要表达的是无条件的向右值转换,而std::forward仅对绑定到右值的引用才实施向右值的转换。这两个是非常不同的行为。前者是为移动操作做铺垫,后者是维持对象类型不变来转发。

Things to Remember

  • std::move执行向右值的无条件强制转换。就其本身而言,它不会移动任何东西;
  • 仅当传入的实参被绑定到右值时,std::forward才针对该实参执行向右值的强制类型转换;
  • 运行时,std::move 和 std::forward都不会做任何事情;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值