条款23:理解std::move和std::forward

为了了解std::movestd::forward,一种有用的方式是从它们不做什么这个角度来了解它们。std::move不移动(move)任何东西,std::forward也不转发(forward)任何东西。在运行时,它们不做任何事情。它们不产生任何可执行代码,一字节也没有。

std::movestd::forward仅仅是执行转换(cast)的函数(事实上是函数模板)。std::move无条件的将它的实参转换为右值,而std::forward只在特定情况满足时下进行转换。它们就是如此。这样的解释带来了一些新的问题,但是从根本上而言,这就是全部内容。

为了使这个故事更加的具体,这里是一个C++11的std::move的示例实现。它并不完全满足标准细则,但是它已经非常接近了。

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);
}

我为你们高亮了这段代码的两部分(译者注:高亮的部分为函数名movestatic_cast<ReturnType>(param))。一个是函数名字,因为函数的返回值非常具有干扰性,而且我不想你们被它搞得晕头转向。另外一个高亮的部分是包含这段函数的本质的转换。正如你所见,std::move接受一个对象的引用(准确的说,一个通用引用(universal reference),见条款24),返回一个指向同对象的引用。

该函数返回类型的&&部分表明std::move函数返回的是一个右值引用,但是,正如条款28所解释的那样,如果类型T恰好是一个左值引用,那么T&&将会成为一个左值引用。为了避免如此,type trait(见条款9)std::remove_reference应用到了类型T上,因此确保了&&被正确的应用到了一个不是引用的类型上。这保证了std::move返回的真的是右值引用,这很重要,因为函数返回的右值引用是右值。因此,std::move将它的实参转换为一个右值,这就是它的全部作用。

此外,std::move在C++14中可以被更简单地实现。多亏了函数返回值类型推导(见条款3)和标准库的模板别名std::remove_reference_t(见条款9),std::move可以这样写:

template<typename T>
decltype(auto) move(T&& param)          //C++14,仍然在std命名空间
{
    using ReturnType = remove_referece_t<T>&&;
    return static_cast<ReturnType>(param);
}

看起来更简单,不是吗?

因为std::move除了转换它的实参到右值以外什么也不做,有一些提议说它的名字叫rvalue_cast之类可能会更好。虽然可能确实是这样,但是它的名字已经是std::move,所以记住std::move做什么和不做什么很重要。它只进行转换,不移动任何东西。

当然,右值本来就是移动操作的候选者,所以对一个对象使用std::move就是告诉编译器,这个对象很适合被移动。所以这就是为什么std::move叫现在的名字:更容易指定可以被移动的对象。

事实上,右值只不过经常是移动操作的候选者。假设你有一个类,它用来表示一段注解。这个类的构造函数接受一个包含有注解的std::string作为形参,然后它复制该形参到数据成员。假设你了解条款41,你声明一个值传递的形参:

class Annotation {
public:
    explicit Annotation(std::string text);  //将会被复制的形参,
    …                                       //如同条款41所说,
};                                          //值传递

但是Annotation类的构造函数仅仅是需要读取text的值,它并不需要修改它。为了和历史悠久的传统:能使用const就使用const保持一致,你修订了你的声明以使text变成const: 

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

当复制text到一个数据成员的时候,为了避免一次复制操作的代价,你仍然记得来自条款41的建议,把std::move应用到text上,因此产生一个右值: 

class Annotation {
public:
    explicit Annotation(const std::string text)
    :value(std::move(text))    //“移动”text到value里;这段代码执行起来
    { … }                       //并不是看起来那样
    
    …

private:
    std::string value;
};

这段代码可以编译,可以链接,可以运行。这段代码将数据成员value设置为text的值。这段代码与你期望中的完美实现的唯一区别,是text并不是被移动到value,而是被拷贝。诚然,text通过std::move被转换到右值,但是text被声明为const std::string,所以在转换之前,text是一个左值的const std::string,而转换的结果是一个右值的const std::string,但是纵观全程,const属性一直保留。

当编译器决定哪一个std::string的构造函数被调用时,考虑它的作用,将会有两种可能性:

class string {                  //std::string事实上是
public:                         //std::basic_string<char>的类型别名
    …
    string(const string& rhs);  //拷贝构造函数
    string(string&& rhs);       //移动构造函数
    …
};

在类Annotation的构造函数的成员初始化列表中,std::move(text)的结果是一个const std::string的右值。这个右值不能被传递给std::string的移动构造函数,因为移动构造函数只接受一个指向non-conststd::string的右值引用。然而,该右值却可以被传递给std::string的拷贝构造函数,因为lvalue-reference-to-const允许被绑定到一个const右值上。因此,std::string在成员初始化的过程中调用了拷贝构造函数,即使text已经被转换成了右值。这样是为了确保维持const属性的正确性。从一个对象中移动出某个值通常代表着修改该对象,所以语言不允许const对象被传递给可以修改他们的函数(例如移动构造函数)。

从这个例子中,可以总结出两点。第一,不要在你希望能移动对象的时候,声明他们为const。对const对象的移动请求会悄无声息的被转化为拷贝操作。第二点,std::move不仅不移动任何东西,而且它也不保证它执行转换的对象可以被移动。关于std::move,你能确保的唯一一件事就是将它应用到一个对象上,你能够得到一个右值。

关于std::forward的故事与std::move是相似的,但是与std::move总是无条件的将它的实参为右值不同,std::forward只有在满足一定条件的情况下才执行转换。std::forward有条件的转换。要明白什么时候它执行转换,什么时候不,想想std::forward的典型用法。最常见的情景是一个模板函数,接收一个通用引用形参,并将它传递给另外的函数:

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));
}

考虑两次对logAndProcess的调用,一次左值为实参,一次右值为实参:

Widget w;

logAndProcess(w);               //用左值调用
logAndProcess(std::move(w));    //用右值调用

logAndProcess函数的内部,形参param被传递给函数process。函数process分别对左值和右值做了重载。当我们使用左值来调用logAndProcess时,自然我们期望该左值被当作左值转发给process函数,而当我们使用右值来调用logAndProcess函数时,我们期望process函数的右值重载版本被调用。

但是param,正如所有的其他函数形参一样,是一个左值。每次在函数logAndProcess内部对函数process的调用,都会因此调用函数process的左值重载版本。为防如此,我们需要一种机制:当且仅当传递给函数logAndProcess的用以初始化param的实参是一个右值时,param会被转换为一个右值。这就是std::forward做的事情。这就是为什么std::forward是一个有条件的转换:它的实参用右值初始化时,转换为一个右值。

你也许会想知道std::forward是怎么知道它的实参是否是被一个右值初始化的。举个例子,在上述代码中,std::forward是怎么分辨param是被一个左值还是右值初始化的? 简短的说,该信息藏在函数logAndProcess的模板参数T中。该参数被传递给了函数std::forward,它解开了含在其中的信息。该机制工作的细节可以查询条款28。

考虑到std::movestd::forward都可以归结于转换,它们唯一的区别就是std::move总是执行转换,而std::forward偶尔为之。你可能会问是否我们可以免于使用std::move而在任何地方只使用std::forward。 从纯技术的角度,答案是yes:std::forward是可以完全胜任,std::move并非必须。当然,其实两者中没有哪一个函数是真的必须的,因为我们可以到处直接写转换代码,但是我希望我们能同意:这将相当的,嗯,让人恶心。

std::move的吸引力在于它的便利性:减少了出错的可能性,增加了代码的清晰程度。考虑一个类,我们希望统计有多少次移动构造函数被调用了。我们只需要一个static的计数器,它会在移动构造的时候自增。假设在这个类中,唯一一个非静态的数据成员是std::string,一种经典的移动构造函数(即,使用std::move)可以被实现如下:

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)                    //不自然,不合理的实现
    : s(std::forward<std::string>(rhs.s))
    { ++moveCtorCalls; }

    …

}

注意,第一,std::move只需要一个函数实参(rhs.s),而std::forward不但需要一个函数实参(rhs.s),还需要一个模板类型实参std::string。其次,我们传递给std::forward的类型应当是一个non-reference,因为惯例是传递的实参应该是一个右值(见条款28)。同样,这意味着std::move比起std::forward来说需要打更少的字,并且免去了传递一个表示我们正在传递一个右值的类型实参。同样,它根绝了我们传递错误类型的可能性(例如,std::string&可能导致数据成员s被复制而不是被移动构造)。

更重要的是,std::move的使用代表着无条件向右值的转换,而使用std::forward只对绑定了右值的引用进行到右值转换。这是两种完全不同的动作。前者是典型地为了移动操作,而后者只是传递(亦为转发)一个对象到另外一个函数,保留它原有的左值属性或右值属性。因为这些动作实在是差异太大,所以我们拥有两个不同的函数(以及函数名)来区分这些动作。

请记住:

  • std::move执行到右值的无条件的转换,但就自身而言,它不移动任何东西。
  • std::forward只有当它的参数被绑定到一个右值时,才将参数转换为右值。
  • std::movestd::forward在运行期什么也不做。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: c++中的std::move和std::forward都是用于实现完美转发的工具。 std::move是将一个左值强制转换为右值引用,从而实现将资源所有权从一个对象转移到另一个对象的目的。使用std::move可以避免不必要的复制和赋值操作,提高程序的效率。 std::forward则是用于在函数模板中实现完美转发,将参数按照原来的类型转发给下一个函数。它可以保证参数的类型和值被完美地转发,避免了不必要的拷贝和移动操作,提高了程序的效率。 总的来说,std::move和std::forward都是用于提高程序效率和避免不必要的拷贝和移动操作的工具。 ### 回答2: C++标准库中提供了两个模板函数std::move和std::forward,它们在C++11中引入,用于实现移动语义和完美转发。 std::move的作用是将一个左值强制转换为右值引用,使得该对象的所有权能够被转移,而不是进行复制或者赋值。通过调用移动构造函数或者移动赋值运算符来减少开销。移动语义是C++11中的一个重要特性,它可以提高程序的效率并且使得程序更加高效。 std::forward的作用是实现完美转发,将函数参数原封不动地转发到另一个函数中,使得函数模板可以保持参数类型和实参类型一致。std::forward用于实现通用类型的泛型编程,解决了模板函数中参数类型无法确定的问题。 实际上,std::move和std::forward的实现方式都非常简单,都是使用了static_cast进行类型转换。但是它们在C++11中的引入,以及其实现的本质却给C++程序的效率提高和泛型编程提供了重要的支持。 总之,std::move和std::forwardC++11中非常重要的语言特性,它们可以帮助程序员实现移动语义和完美转发,提高程序的性能和可读性。要注意正确使用它们,以避免出现不必要的开销和错误。 ### 回答3: C++ 11中引入了两个新的特殊函数模板std::move()和std::forward(),用来实现完美转发和移动语义,提高了代码的效率和简洁性。 std::move的作用就是将一个左值转换成右值引用,将左值的所有权抢过来,但不进行任何内存拷贝。通常用于移动语义,可以提高程序的效率。用法很简单,就是std::move(左值变量)。比如,若有个vector<int> a和一个vector<int> b,我想把b中的元素全部移动到a中,可以这样写:a.insert(a.end(), std::make_move_iterator(b.begin()), std::make_move_iterator(b.end()));这里,std::make_move_iterator()是一个语法糖,将它们的元素包装成可以引用的右值。 std::forward的作用是保持参数本来的类型(左值或右值),既可以接收左值也可以接收右值,并将参数传递给其他函数,这就是所谓的完美转发。完美转发可以达到只有一个函数就可以处理所有情况的目的。用法就是std::forward<参数类型>(参数变量)。比如,若有个函数template<class T> void f(T&& t),其中参数t是万能引用,需要把t传递给其他函数g(),我们可以这样写:g(std::forward<T>(t));这样就可以达到完美转发的目的。 需要注意的是,std::move和std::forward虽然看起来相似,但作用是不同的,std::move是将左值转换成右值引用,而std::forward是维持参数的原类型,用于完美转发。同时,它们都需要加上相应的模板类型,以便让编译器进行类型推导。在使用时,需要根据情况选择合适的函数,以达到更好的效果。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值