注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

还东国的博客

行之苟有恒,久久自芬芳

 
 
 

日志

 
 

(转载)条款23:理解std::move和std::forward  

2015-10-14 14:53:59|  分类: C++(VC)编程 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |

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

2015-03-20 17:12 450人阅读 评论(0) 收藏 举报

http://blog.csdn.net/coolmeme/article/details/44459999

通过了解std::movestd::forward不做什么来理解它们很有用。std::move不移动任何东西,std::forward也不转移任何东西。在运行时(runtime),他们什么都不做,一行代码也不产生。

 

std::movestd::forward仅仅是进行类型转换的函数(实际上是函数模板)。std::move无条件的将其参数转换为右值,而std::forward只在必要情况下进行这个转换,就是这样。这个解释会引起一系列问题,但是基本上这就是完整的故事。

 

为了让故事更加具体,这有一个在c++11std::move的模拟实现:

 

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的参数为一个对象的引用(统一引用,详见条款24),并且返回的也是该对象的引用。

 

函数返回值的“&&”部分表示std::move返回了一个右值引用,但是如条款28所述,假如类型T碰巧是个左值引用,T&&就会成为一个左值引用。为阻止这个发生,一个type trait(见条款9std::remove_reference被用在T上,这样可以确保“&&”可以应用在一个不是引用的类型上,这个很重要,因为函数返回的右值引用必须是右值。于是,std::move将其参数转换右值,这就是它所有做的事情。

 

此外,std::movec++14中的实现显得更简短。由于函数返回类型推导(条款3)和标准库里的别名模板std::remove_reference_t (见条款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做什么和不做什么很重要。它做的是转换,不做移动。

 

当然,右值适合被移动,所以std::move应用在一个对象上时就是告诉编译器对象可以被移动。这就是为什么std::move拥有这样的名字:使得指定可以被移动的对象更容易些。

 

事实上,右值是仅有的可以被移动的对象。假如你写了个类代表注释(annotation),类的构造函数使用std::string类型做为参数表示注释内容,并且拷贝参数到一个成员变量里。根据条款41的信息,你声明了一个传值参数:

 

class Annotation {

public:

  explicit Annotation(std::string text); // param to be copied,

 

  …                                                    // so per Item 41,

};                                                       // pass by value

 

但是Annotation的构造函数仅仅需要读取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))                                      // "move" text into value; this code

  { … }                                                                   // doesn't do what it seems to!

  …

private:

  std::string value;

};

 

代码编译链接都可以正常,也把设置了数据成员为text的内容。唯一使得这段代码和你想象中的完美实现不一样的地方是text不是移动到value的,是拷贝的。当然,text是通过std::move转换成一个右值,但是text是被声明成const std::string,所以在转换前,text是一个左值的const std::string,转换的结果是一个右值const std::string,最终常量性保留了下来。

 

考虑下当编译器必须决定哪个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 move移动构造函数

    …

};

 

Annotation的构造函数的成员初始化列表里,std::move(text)的结果是一个类型为const std::string的右值。这个右值不能传递给std::stringmove构造函数,因为move构造函数需要一个指向非常量std::string的右值引用作为参数。然而这个右值可以传递给拷贝构造函数,因为指向常量的左值引用是允许绑定到一个常量右值的。于是成员初始化就会调用std::string的拷贝构造函数,即使text已经转换成一个右值。这样的行为是维持常量正确性的一个必需,因为移动一个对象到另一个值通常会改变这个对象,所以语言就不应该允许常量对象被传递给一个能修改他们的函数(比如move构造)。

 

从这个例子可以学到两个教训。第一,假如你想对象能够被移动,不要声明对象为const,在const对象上的移动操作默默的被转换成了拷贝操作。第二,std::move不仅不移动任何东西,甚至不能保证被转换的对象可以被移动。唯一可以确认的是应用std::move的对象结果是个右值。

 

 

 

std::forward的故事和std::move的故事很类似,但std::move是无条件的转换其参数为一个右值,而std::forward是在某些特定条件下进行转换。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被传递给函数processprocess被重载为左值和右值。当我们通过左值去调用logAndProcess时,我们很自然的期望那个左值可以同样作为一个左值转移到process函数,当我们通过右值去调用logAndProcess时,我们期望重载的右值函数可以被调用。

 

但是param,像所有函数参数一样,是个左值。在logAndProcess内部每一个对process的调用会调起左值重载。为了避免这个,我们需要一个机制来把param转换成一个右值,当且仅当传入的用来初始化parm的实参(就是传递到logAndProcess的参数)是个右值。

 

你可能疑惑std::forward是怎么知道它的参数是通过一个右值来初始化的。比如在以上代码中,std::forward是怎么知道param是被一个左值或右值来初始化呢?简单的回答是这个信息被编码到logAndProcess的模板参数T里面。这个参数被传递给std::forward,然后恢复了编码的信息。详细的描述见条款28

 

既然std::movestd::forward都归结为转换,唯一不同就是std::move始终进行转换,而std::forward仅仅有时候转换,你可能会问我们是否可以废弃std::move,而只用std::forward。从纯技术角度来看,答案是肯定的:std::forward可以做到,std::move不是必须的。当然函数也不是必须的,我们可以写转换代码,但是我希望我们会觉得那样比较恶心。

 

std::move吸引人之处在于方便,减少了错误的可能性,而且更加清晰。考虑一个类,可以跟踪我们使用了多少次的move构造函数。我们所需要的是一个静态变量的计数器,在move构造函数使用时增加。假设类里面的唯一一个非静态数据成员是std::string,这里有个方便的方法(也就是利用std::move)去实现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)                      // unconventional,

: s(std::forward<std::string>(rhs.s))  // undesirable

{ ++moveCtorCalls; }                        // implementation

};

 

注意首先std::move只需要一个函数参数(rhs.s),而std::forward既需要一个函数参数(rhs.s),又需要一个模板类型参数(std::string)。然后我们注意到我们传递给std::forward的参数类型必须一个非引用的,因为编码规范上说明了传递的参数必须是一个右值(见条款28)。std::move需要更少的打字,and it spares us the trouble of passing a type argument。也减少了我们传递一个错误类型(比如std::string&,这会导致数据成员s被拷贝构造而不是move构造)的可能性。

 

更重要的是,使用std::move会无条件转换为一个右值,而使用std::forward只会把是绑定到右值引用的参数转换成右值。这是两个非常不同的行为。第一个是典型的move,而第二个是仅仅传递(转移)一个对象到另一个函数,通过这种办法来保留对象原始的左值性或右值性。因为两者如此不同,所以最好我们使用不同的函数(名)来区分它们。

 

                                          要记住的事情

 

1.std::move执行一个无条件的转化到右值。它本身并不移动任何东西;

 

2.std::forward把其参数转换为右值,仅仅在那个参数被绑定到一个右值时;

 

3.std::movestd::forward在运行时(runtime)都不做任何事。

  评论这张
 
阅读(258)| 评论(0)
推荐 转载

历史上的今天

在LOFTER的更多文章

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017