C_and_CPP 板


LINE

这篇文章的原始出处是 Visual C++ Team Blog 的 Rvalue References: C++0x Features in VC10, Part 2,作者是 Stephan T. Lavavej。 (原始网址:http://tinyurl.com/d7k8xk) 这篇文章是获得原作者 Stephan 的许可之後进行中文翻译的, 并依照 Stephan 的要求,我在此注明这篇文章不是官方的,是我个人的翻译文章。 翻译上如果有任何错误请告诉我,谢谢,yoco。 阅读之前有两件非常重要的事情要注意,尤其是第二点: 1. 这篇文章「相当长」,所以我没有力气再帮 bbs 版本上色了, 可以的话请直接看这个 pdf 版本,会比较舒服。 http://peter.xiau.googlepages.com/rvalue_ch.pdf 2. 我这个人非常的虚荣,所以请务必推荐这篇文章,不然我会睡不着。 ==== 以下是正文 ==== Rvalue References: C++0x Features in VC10, Part 2 Part 1 of this series covered lambdas, auto, and static_assert. http://tinyurl.com/6bvzbg 今天要讲的是 rvalue references,rvalue reference 可以让我们作到两件事情:move 语意,还有完美转发。move 语意还有完美转发这两件事常常让很多人头大,因为他们的 差异在於 lvalue reference 跟 rvalue reference,而只有非常少数的 C++98/03 程式 设计师很熟知 lvalue reference 跟 rvalue reference 的差别。这篇文章很长,因为我 会把 rvalue reference 如何运作的机制解释得非常清楚。 免惊,rvalue reference 用起来很简单,比听起来简单多了。至於要怎麽在自己的程式 码里面实做 move 语意跟完美转发,只要依照我後面示范的例子就可以了。学习 rvalue reference 跟 move 语意是绝对值回票价的,他可以让你的程式效能有数量级的增进,而 完美转发让你可以轻轻松松的写出高度泛型的程式码。 Lvalue and rvalue in C++98/03 要了解 C++0x 的 rvalue references,你必须要先了解 C++98/03 的 rvalue reference 。 「lvalue」跟「rvalue」这两个术语很容易被搞混,因为他们的历史渊源本来就很混淆。 (顺带一提,这两个术语的发音是「L values」跟「R values」,虽然他们通常都写成一 个单字。)这两个观念是从 C 继承而来的,而到了 C++ 这两个观念被探究的更清楚。为 了节省时间,我就不谈他们的历史,也不谈他们为什麽叫做「lvalue」跟「rvalue」,我 会直接说他们在 C++98/03 是怎麽运作的。(好吧,其实也不是什麽大秘密:「L」代表 「left」而「R」代表「right」。但是现在这两个观念已经演化了,lvalue 跟 rvalue 这两个名字已经不能精准的表达他们现在实际上的意义。与其帮你上一整堂的历史课,不 如直接想一想「上夸克」跟「下夸克」的差别,这样应该就懂了。) C++03 3.10/1 提到:「Every expression is either an lvalue or an rvalue.(每一 个运算式要嘛是一个 lvalue,不然就是一个 rvalue)」这很重要,请一定要记住, lvalue 跟 rvalue 所指的对象,是运算式,而不是物件(object)。 Lvalue 指的是在单一一个运算式之後所代表的一个续存物件(persistent object,也就 是非暂时物件),举例来说,obj,*ptr,ptr[index],还有 ++x 都是 lvalue。 Rvalue 则是那些当整个运算式一结束的时候就会消失的无影无踪的暂时物件。举例来说 :1729,x + y,std::string("meow"),还有 x++ 都是 rvalue。 注意一下 ++x 跟 x++ 的差别。当我们写 int x = 0; 的时候,x 是一个 lvalue,因为 他代表的是一个会持续存在的物件。运算式 ++x 也是一个 lvalue,这个运算式改变了 x 的值,并且赋予他一个新的名字「++x」,但是实际上他依然代表 x 这个续存物件。 然而,运算式 x++ 却是一个 rvalue,他把本来的续存物件复制了一份,然後改变续存物 件的值,最後把复制出来的值传回去。而这个复制出来的物件是一个暂时物件,运算式结 束之後,这个物件就消失了。++x 跟 x++ 都会增加 x 值,但是 ++x 传回一个续存物件 ,也就是他自己,而 x++ 传回的是一个暂时的复制品。这就是为什麽 ++x 是一个 lvalue,而 x++ 是一个 rvalue。一个运算式是 lvalue 还是 rvalue,跟这个运算式做 了些什麽没有关系,只跟他代表的东西有关系,端看这个运算式是一个续存物件,还是一 个暂时物件。 如果你想要从另外一个直觉的角度来理解一个运算式是不是 lvalue,那你只要问自己: 「我能不能对这个运算式取址(address of, &)?」如果可以,那这个运算式就是一个 lvalue,如果不行,那就是一个 rvalue。举例来说: &obj,&*ptr[index],还有 &++x 都是合法的运算式(有的例子虽然很笨,但他们还是合法的)。而 &1729,&(x + y),&std::string("meow"),还有 &x++ 都是不合法的运算式。为什麽这个判断法有效? 根据 C++03 5.3.1/2 当我们对一个运算元取址的时候,这个运算元必须是一个左值。那 为什麽标准要这样定?因为对一个续存物件取址很安全,没有问题,但是对一个暂时物件 取值,是非常危险的行为,因为这个暂时物件马上就会蒸发在空气里面,而你拿着他的记 忆体位址,你不知道你会改到什麽东西。 前面所讲的用来解释 lvalue 跟 rvalue 的例子,在运算子覆载(operator overloading )的时候并不成立。根据 C++03 5.2.2/10「A function call is an lvalue if and only if the result type is a reference(只有当一个函数传回一个 reference 的时 候,呼叫这个函数得到的结果才会是一个 lvalue)」而运算子覆载本质上也是函数。所 以当我们写 vector<int> v(10, 1729); 的时候,v[0] 是一个 lvalue,因为 operator[]() 的传回型态是 int&,而且 &v[0] 是合法的运算式,也很好用。而当我们 写 string s("foo"); string t("bar"); 的时候,s + t 是一个 rvalue,因为 operator+() 传回一个 string,当然 &(s + t) 也是不合法的。 Lvalue 跟 rvalue 都有可变动的(modifiable, non-const)跟不可变动的( non-modifiable, const)两种,举例来说: string one("cute"); const string two("fluffy"); string three() { return "kittens"; } const string four() { return "are an essential part of a healthy diet"; } one; // modifiable lvalue two; // const lvalue three(); // modifiable rvalue four(); // const rvalue 一个 Type& 可以系结(bind)到一个可变动的 lvalue(译注:意思就是说使用一个可变 动的 lvalue 来初始化你的 reference。比方说 int a; int& t = a;),你可以透过这 个 reference 来读取原有的值跟写入新的值。但是不能你不能系结到一个 const lvalue ,因为这样会违反常数性。你也不能把他系结到一个 non-const rvalue,这样很危险。 当你修改暂时变数的时候,可能会引来一些度烂的隐微 bug,所以 C++ 很明智的禁止这 种行为了。(我应该要提一下,VC 有一个很邪恶的扩充,让你可以作到这件事情,但是 如果你编译的时候加上 /W4 参数,那编译器就会警告你说你用到了这个扩充功能。一般 来说会警告啦~)你也不能用 const rvalue 来初始化一个 type&,这种行为是智障加三 级。(细心的读者应该会发现我这边没有提到 template 引数的推导)(译注:/W4 是 VC 最高的警告标准,预设是 /W3) 一个 const Type& 可以用来系结到任何一种型态:可变动的 lvalue,不可变动的 lvalue,可变动的 rvalue,还有不可变动的 rvalue。(然後你就可以透过这个 reference 来观察这些被系结的对象) 每一个 reference 都有一个名字,所以一个系结到 rvalue 的 reference,他本身是一 个 lvalue(没错!Lvalue!)(译注:叫的出名字的,比方说 int& a;,当然就不是暂 时变数,而是一个具体存在的物件,那当然是 lvalue)。(那如果是一个系结到 rvalue 的 const reference 呢,就是一个 const lvalue)这很容易让人搞混,但是这 点跟之後要讲的东西有很大的关系,所以我现在必需要解释的很清楚。假设有一个函数 void observe(const string& str),这个函数内部的实做,str 是一个const lvalue, 只要在这个函数结束之前,这个 str 就可以被取址,取出来的位址也可以被使用,这点 就算当初呼叫这个函数的时候,我们传给他的是一个 rvalue 也一样。就像上面的 three() 跟 four() 一样。当我们呼叫 ovserve("purr") 的时候,我们会先根据 "purr" 建立一个暂时的 string 物件,然後 str 就会系结到这个暂时物件(是个 rvalue)。three() 跟 four() 的传回值没有名字,所以是 rvalue,但是在 observe() 里面,str 这东西有个名字,当然他就是一个 lvalue,正如我前面说过的啦 :「Lvalue 跟 rvalue 所指的对象,是运算式,不是物件。」因为 str 可以系结到一个 运算式的结果,也可能是一个马上就会消失的暂时变数,所以当 observe() 结束之後, 我们就不应该在任何地方保存这个暂时变数的位址。 那你有没有曾经对一个系结到 rvalue 的 const reference 取址过?你有!什麽时候? 就是当你撰写一个拷贝运算子 Foo& operator=(const Foo& other) 而且这个拷贝运算字 有作「自我赋值」检查 if (this != &other) { copy stuff; } return *this; 然後当 你拷贝一个暂时变数 Foo make_foo(); 的时候,你就对一个 rvalue const reference 取值啦。 这个时候,你可能会问说:「那请问 const rvalue 跟 non-const rvalue 有什麽不一样 ?如果我宣告一个 non-const Type&,我不能把这个 Type& 系结到任何的 non-const rvalues,如果我宣告一个 const Type&,我也一样没办法透过这个 reference 去改我系 结到的对象。总之我都改不到 rvalue,那到底我要怎麽样才有办法改变我系结到的 rvalue?」在 C++98/03 的标准呢,这两者还是有一点点很细微的差异的:non-const 的 rvalue 可以呼叫 non-const 的成员函数。C++ 不想让你不小心变更一个暂时变数的 值,但是当你直接呼叫一个 non-const 成员函数的时候,语意是非常明显的,不可能是 不小心,所以 C++ 允许你这麽作。而到了 C++0x,这个问题的答案有了剧烈的转变:你 可以透过这点来作到 move 语意。 贺!现在你已经有我所谓的「lvalue/rvalue 观」,这让你能够看清楚一个运算式到底是 lvalue 还是 rvalue,在加上你本来就会的「const 观念」,你现在已经完全可以理解 为什麽当一个函数是 void mutate(string &ref) 时,mutate(one) 是合法的。但是 mutate(two),mutate(three()),mutate(four()) 跟 mutate("purr") 却是不合法的了 (one,two,three 请参照上面程式码的定义)。如果你是一个 C++98/03 程式设计师, 你本来就可以凭你的直觉(或是你的 compiler)分辨出这些东西哪些合法哪些不合法, 你的直觉警报器会喔咿喔咿的跟你说 mutate(three()) 是个鱼目混珠的伪物。但是现在 你所拥有的「lvalue/rvalue 观」让你更明确理解为什麽 three() 是一个 rvalue,也知 道为什麽 non-const reference 不能被系结到 rvalue。那知道这些东西有什麽用?对那 些专门钻研程式语言规则条例的「语言律师」来说,有用,但是对普通的程式设计师没什 麽用。毕竟就算你都不知道这些细节好了,其实对你也没多大影响。但是重点来了:相较 於 C++98/03,C++0x 的 lvalue/rvalue 观念是非常非常有用的东西。(特别是当你可以 分辨一个运算式是 rvalue 还是 rvalue,是 const 还是 non-const,然後根据这个差别 做不同的事情。)为了要更有效的使用 C++0x 你必须要有 lvalue/rvalue 的观念,现在 万事具备,我们可以开工啦! the copying problem C++98/03 结合了不可思议的高度抽象化以及不可思议的效能,但是还是有个问题: C++98/03 超爱复制物件的。因为 value 语意,复制出来的物件是独立的个体,修改复制 出来的物件,不会影响本来物件的值。value 语意是很棒没错,但是也同时带来了很多不 必要的复制成本,像是 vector 跟 string 这类的 heavy 物件的复制就很昂贵。( heavy 是说复制起来很昂贵,比方说一个上百万元素的 vector 的复制成本就很贵。) Return Value Optimization (RVO) 跟 Named Return Value Optimization (NEVO) 这两 种最佳化技术,在某些状况下可以省掉拷贝建构子,减轻这个成本,但是还是没有根治这 个问题。 最无谓的复制成本,就是当那些来源物件准备要被销毁的时候。请问你会不会拷贝一份文 件之後马上把原稿销毁?这样不是很浪费吗。你一开始就拿着原稿就好啦,何必多费事。 下面是我从一份标准委员会文件的范例改来的,我所谓的「杀手范例」,假设你有一堆 string 像是这样… string s0("my mother told me that"); string s1("cute"); string s2("fluffy"); string s3("kittens"); string s4("are an essential part of a healthy diet"); 然後你想要像这样把他们接起来: string dest = s0 + " " + s1 + " " + s2 + " " + s3 + " " + s4; 这样作效率如何?(当然单指这个特例的话,我们也不用伤脑筋,因为反正他不用百万分 之一秒就可以作完了,但是我们讨论的是一个更一般性的,一个语言层面的现象。) 每次呼叫 operator+() 的时候,就会传回一个暂时物件,而这边呼叫了八次,所以有八 个暂时物件。每呼叫一次,他们的建构子就会作一次动态记忆体配置,拷贝目前已经拷贝 的所有的字元,然後,马上把他们销毁,作一次动态记忆体释放。(如果你听说过所谓的 Small String Optimization,VC 附的 STL 实作有用上这个技术,这东西可以省去动态 记忆体配置的成本。可惜这招在这边不适用,因为我很故意的选了一个够长的 s0,所以 就算你用上那个技术,你还是什麽都省不到!可能你也听过一个叫做 Copy-On-Write 的 最佳化技术,算了吧,这招不是用在这的,而且现在也没人在用这东西了,因为在 multithread 的环境下这东西就绝体绝命啦。)(译注:我们还有 expression template 可以用,STLport 的 std::string 就有用这个技术,当你在串接 string 的时 候,其实他没有真的在把他们串起来,他只是先用一个 proxy class 把你要串接的 string 记住,等到你要把结果指派出去的时候,他再一次把这些东西加起来。) 事实上呢,因为每一个被串接出来的字串,都还再被拿去串耶,所以事实上那个时间复杂 度是字串长度的平方,妈阿,真浪费。这点实在让 C++ 很尴尬。事情怎麽会搞成这样? 有没有改善的方法? 现在问题的点是这样,operator+() 接收两个参数,一个是 const string&,另外一个是 const string& 或是 const char*(还有其他的覆载版本,但是这边我们没用到),但 是呢,这个 operator+() 没办法分辨你塞给他的是 lvalue 还是 rvalue,所以他只好先 建立一个暂时物件,然後再把那个暂时物件传回来。那为什麽这边会跟 lvalue/rvalue 有关? 当我们要算 s0 + " " 的值的时候,我们一定要建立一个暂时物件。s0 是一个 lvalue, 也就是是一个续存物件。所以我们不能去修改他。(好!有人注意到了!)但是当我们要 算 (s0 + " ") + s1 的时候,我们可以直接把 s1 的内容接在那个暂时物件的後面嘛, 这样我们就不用建立第二个暂时物件然後还要把第一个丢掉。move 语意的核心观念:因 为 s0 + " " 是一个 rvalue,是一个运算式的结果产出的暂时物件,除了他自己,没有 人有办法动到他。如果我们可以徵测到一个 non-const rvalue,那我们就可以任意修改 里面的值,而且还没有人会发现。operator+() 不该去修改他的引数,但是如果他们修改 的是一个 rvalue,那,谁在乎?我们可以用这个方法,把全部的字元都串接到单一一个 暂时物件上面去。这个手法完全消除了不必要的复制成本跟动态记忆体配置成本,留下的 是一个线性的复杂度,赞! 技术上来说,在 C++0x,其实你每次呼叫 operator+() 的时候还是会传回一个独立的暂 时物件。只是当你建立第二个暂时物件((s0 + " ") + s1)的时候,他会从第一个暂时 物件(s0 + " ")那边把记忆体偷过来,然後把 s1 的内容接在那块记忆体後面(这也可 能导致一个二次时间复杂度的记忆体重置动作)。「偷」这个动作呢,由指标的操作来达 成:首先我们把第一个暂时物件的内部的指标拷贝到第二个暂时物件来,然後把第一个暂 时物件的内部指标清成 null。这样当第一个暂时物件要被销毁的时候,因为他内部的指 标是个 null,所以解构子啥也不会作。 广义的说,当我们有办法徵测 non-const rvalue 的时候,我们就有办法作到「剽窃资源 」这件事。当一个物件实际上系结到的是一个 non-const rvalue,且如果这个物件有掌 握某些资源(比方说记忆体),那我们就可以直接偷过来,而不用像以前那样要复制他们 ,反正他们马上就要蒸发了。当你要从一个 rvalue 建立一个物件,或是当你要指派某个 rvalue 物件的值给另外一个物件的时候,这个偷取他们资源的行为,被归类成「moving 」,然後可以被 move 的物件就具有「move 语意」。 这个观念在很多地方都超有用的,比方说 vector 的重新配置。当一个 vector 需要更多 空间的时候(例如 push_back() 的时候),需要重新配置记忆体,然後把 vector 里面 的每一个元素从旧的记忆体拷贝到新的记忆体上面。而这些拷贝的动作可能很贵(比方说 一个 vector<string>,每一个 string 都要复制,都需要一次动态记忆体配置)。等等 !旧的 vector 里面的元素马上就要被销毁了耶,所以其实我们可以把里面的东西 move 到新的那一个去,这样就不用复制他们了。在这种状况下,旧的记忆体区块里面的每一个 元素都占据一个续存的记忆体空间,而且你用来存取他们的运算式也是一个左值(比方说 old_ptr[index]),在重新配置记忆体的过程中,我们用 non-const rvalue reference 来系结到这些元素,假装这些旧的记忆体区块是 non-const rvalue 有个好处 ,就是我们可以把里面的东西 move 出来,省下复制物件的成本。(当我说「我要把那些 lvalue 当作是 non-const rvalue」的时候,等同於「我知道那些东西是续存的 lvalue ,但是我不在乎这些 lvalue 会变成怎样。反正这个 lvalue 就要被销毁了,或是等等就 会被指派新的值,管他的,反正我不在乎。所以如果我能从里面偷东西,那我干嘛不偷。 」) C++0x 的 rvalue reference 让我们可以徵测到 non-const rvalue 并且从里面干东西, 而这点让我们作到 move 语意。Rvalue reference 也带给我们「把 lvalue 当作是 non-const rvalue」的能力。接下来我们就要看看 rvalue reference 是怎麽运作的! rvalue references: initialization C++0x 引入了一种新的 reference,rvalue reference,语法是 Type&& 跟 const Type&&。根据目前 C++0x 的草案 N2798 8.3.2/2:「A reference type that is declared using & is called an lvalue reference, and a reference type that is declared using && is called an rvalue reference. Lvalue references and rvalue references are distinct types. Except where explicitly noted, they are semantically equivalent and commonly referred to as references.」(用 Type& 宣 告的 reference 是一个 lvalue reference,用 Type&& 宣告的 reference 是 rvalue reference。lvalue reference 跟 rvalue reference 是不同的型别。除非特别注明的时 候,不然他们在语意上都一样是 reference)这代表你可以把你在 C++98/03 对 reference 的认知直接套用到 C++0x,只需要学他们之间不同的地方。 (我自己习惯把 Type& 念作「Type ref」,Type&& 念作「Type ref ref」。这样就是 Type 的 lvalue reference 跟 Type 的 rvalue reference。就像是一个常数指标指到 一个 int,我们写作「int * const」,也可以念成「int star const」。) 那他们之间到底差在哪里?Rvalue reference 在初始化以及多载函数决议的时候跟 lvalue reference 有不同的行为。这两者对於初始化的时候,系结的对象以及多载决议 的时候有不同的偏好。 1. Type&,我们已经知道 Type& 只能系结到 non-const lvalue,其他的一概不能。 2. const Type&,我们也知道 const Type& 可以系结到所有的东西。 3. Type&& 可以系结到 non-const lvalue 还有 non-const rvalue,但是不能系结到 const lvalue 跟 const rvalue,因为那样会违反常数性) 4. const Type&& 可以系结到所有的东西。 这些规则看起来就像是火星文,不过其实可以从两条很简单的规则推导出来: 1. 常数性,所以限制你不能把 non-const reference 系结到 const reference。 2. 为了避免不小心改到暂时变数的值,所以限制你把 non-const lvalue reference 系 结到 non-const rvalue。 阿如果你不喜欢看文字描述,比较喜欢看编译器的错误讯息的话,这边给你一个范例: C:\Temp>type initialization.cpp #include <string> using namespace std; string modifiable_rvalue() { return "cute"; } const string const_rvalue() { return "fluffy"; } int main() { string modifiable_lvalue("kittens"); const string const_lvalue("hungry hungry zombies"); string& a = modifiable_lvalue; // Line 16 string& b = const_lvalue; // Line 17 - ERROR string& c = modifiable_rvalue(); // Line 18 - ERROR string& d = const_rvalue(); // Line 19 - ERROR const string& e = modifiable_lvalue; // Line 21 const string& f = const_lvalue; // Line 22 const string& g = modifiable_rvalue(); // Line 23 const string& h = const_rvalue(); // Line 24 string&& i = modifiable_lvalue; // Line 26 string&& j = const_lvalue; // Line 27 - ERROR string&& k = modifiable_rvalue(); // Line 28 string&& l = const_rvalue(); // Line 29 - ERROR const string&& m = modifiable_lvalue; // Line 31 const string&& n = const_lvalue; // Line 32 const string&& o = modifiable_rvalue(); // Line 33 const string&& p = const_rvalue(); // Line 34 } C:\Temp>cl /EHsc /nologo /W4 /WX initialization.cpp initialization.cpp initialization.cpp(17) : error C2440: 'initializing' : cannot convert from 'const std::string' to 'std::string &' Conversion loses qualifiers initialization.cpp(18) : warning C4239: nonstandard extension used : 'initializing' : conversion from 'std::string' to 'std::string &' A non-const reference may only be bound to an lvalue initialization.cpp(19) : error C2440: 'initializing' : cannot convert from 'const std::string' to 'std::string &' Conversion loses qualifiers initialization.cpp(27) : error C2440: 'initializing' : cannot convert from 'const std::string' to 'std::string &&' Conversion loses qualifiers initialization.cpp(29) : error C2440: 'initializing' : cannot convert from 'const std::string' to 'std::string &&' Conversion loses qualifiers 把一个 non-const rvalue 系结到一个 const rvalue 是没问题的。因为 non-const rvalue 的重点就是可以用来修改暂时变数。 虽然 lvalue reference 跟 rvalue reference 在初始化的时候很相似(只有在上面的第 18 跟 28 行不一样),但是这个差异的确改善了多载决议时的解析能力。 rvalue references: overload resolution 函数可以根据 const 跟 non-const lvalue 来进行多载,这你已经很熟了。在 C++0x, 函数还可以根据 const 跟 non-const 的 rvalue 来进行多载。当一个函数有全部的四个 多载版本,你应该可以预期到,每一个函数都根据对应的运算式被呼叫。 C:\Temp>type four_overloads.cpp #include <iostream> #include <ostream> #include <string> using namespace std; void meow(string& s) { cout << "meow(string&): " << s << endl; } void meow(const string& s) { cout << "meow(const string&): " << s << endl; } void meow(string&& s) { cout << "meow(string&&): " << s << endl; } void meow(const string&& s) { cout << "meow(const string&&): " << s << endl; } string strange() { return "strange()"; } const string charm() { return "charm()"; } int main() { string up("up"); const string down("down"); meow(up); meow(down); meow(strange()); meow(charm()); } C:\Temp>cl /EHsc /nologo /W4 four_overloads.cpp four_overloads.cpp C:\Temp>four_overloads meow(string&): up meow(const string&): down meow(string&&): strange() meow(const string&&): charm() 在实务上,你真的去覆载全部四个版本其实没多大用处。真正好玩的是只覆载 const Type& 跟 Type&& 这两个版本: C:\Temp>type two_overloads.cpp #include <iostream> #include <ostream> #include <string> using namespace std; void purr(const string& s) { cout << "purr(const string&): " << s << endl; } void purr(string&& s) { cout << "purr(string&&): " << s << endl; } string strange() { return "strange()"; } const string charm() { return "charm()"; } int main() { string up("up"); const string down("down"); purr(up); purr(down); purr(strange()); purr(charm()); } C:\Temp>cl /EHsc /nologo /W4 two_overloads.cpp two_overloads.cpp C:\Temp>two_overloads purr(const string&): up purr(const string&): down purr(string&&): strange() purr(const string&): charm() 为啥这招有用?理由如下: 1. 初始化规则有「否决权」 2. Lvalue 强烈偏好系结到 lvalue reference,rvalue 强烈偏好系结到 rvalue reference。 3. non-const 运算式轻度的偏向 non-const reference。 (所谓「否决权」是说,当一个候选函数被认为被淘汰了,那他就一点机会都没有了,根 本不会被列入考虑)现在我们一条一条检视这些规则。 1. purr(up),purr(const string&) 跟 purr(string&&) 都没有被否决。但是因为 up 是一个 lvalue,而 lvalue reference 强烈的偏好 lvalue,所以 purr(const string&) 赢了。 2. purr(down),purr(string&&) 因为不满足常数性被否决了,purr(const string&) 又 赢了! 3. purr(strange()),两个人都没有被否决,但是 strange() 是一个 rvalue,而 rvalue 强烈偏好 rvalue reference,non-const 只是轻度的偏向 non-const reference ,所以 purr(string&&) 赢了。 4. purr(charm()),purr(string&&) 因为违反常数性被否决了,所以 purr(const string&) 赢了。 这边要注意到的重点是,当你只覆载 const Type& 跟 Type&& 的时候,non-const rvalue 会系结到 Type&&,然後其他的都系结到 const Type&。就这样,这一组覆载函数 就可以用来作到 move 语意。 重要备注:当一个函数要以 by value 的方法传回值的时候,要把他宣告成 Type 而不是 const Type,因为後者这种宣告方式,会阻绝 Type&& 的系结,造成的结果是 move 语 意最佳化没办法施行。 move semantics: the pattern 这边有一个范例类别,remote_integer,内部保存一个指标,指到一个动态配置来的 int 。(这就是「远端所有权」)对你来说,这个类别的预设建构子、单一参数建构子、拷贝 建构子、指派运算子,还有解构子,应该都是非常熟悉的。在这边我多加了一个 move 语 意的拷贝建构子跟指派运算子,我把这两个函数用 #define MOVABLE 包住,这样我就可 以示范有这两个函数跟没有这两个函数有什麽差别。当然啦,真的写程式的时候不会有人 这样作。 C:\Temp>type remote.cpp #include <stddef.h> #include <iostream> #include <ostream> using namespace std; class remote_integer { public: remote_integer() { cout << "Default constructor." << endl; m_p = NULL; } explicit remote_integer(const int n) { cout << "Unary constructor." << endl; m_p = new int(n); } remote_integer(const remote_integer& other) { cout << "Copy constructor." << endl; if (other.m_p) { m_p = new int(*other.m_p); } else { m_p = NULL; } } #ifdef MOVABLE remote_integer(remote_integer&& other) { cout << "MOVE CONSTRUCTOR." << endl; m_p = other.m_p; other.m_p = NULL; } #endif // #ifdef MOVABLE remote_integer& operator=(const remote_integer& other) { cout << "Copy assignment operator." << endl; if (this != &other) { delete m_p; if (other.m_p) { m_p = new int(*other.m_p); } else { m_p = NULL; } } return *this; } #ifdef MOVABLE remote_integer& operator=(remote_integer&& other) { cout << "MOVE ASSIGNMENT OPERATOR." << endl; if (this != &other) { delete m_p; m_p = other.m_p; other.m_p = NULL; } return *this; } #endif // #ifdef MOVABLE ~remote_integer() { cout << "Destructor." << endl; delete m_p; } int get() const { return m_p ? *m_p : 0; } private: int * m_p; }; remote_integer square(const remote_integer& r) { const int i = r.get(); return remote_integer(i * i); } int main() { remote_integer a(8); cout << a.get() << endl; remote_integer b(10); cout << b.get() << endl; b = square(a); cout << b.get() << endl; } C:\Temp>cl /EHsc /nologo /W4 remote.cpp remote.cpp C:\Temp>remote Unary constructor. 8 Unary constructor. 10 Unary constructor. Copy assignment operator. Destructor. 64 Destructor. Destructor. C:\Temp>cl /EHsc /nologo /W4 /DMOVABLE remote.cpp remote.cpp C:\Temp>remote Unary constructor. 8 Unary constructor. 10 Unary constructor. MOVE ASSIGNMENT OPERATOR. Destructor. 64 Destructor. Destructor. 这边有几点要注意的。 - 对於建构子,我们多载了 copy 跟 move 两种版本,对於指派运算子,我们也多载了 copy 跟 move 两种版本。当我们多载 Type& 跟 const Type&& 的时候,b = square(a); 会自动侦测出可以 move 的状况且施行 move 语意。 - 现在我们直接从别的物件那边偷记忆体,不用再动态配置了。当偷东西的时候,我们拷 贝对方的指标,然後把对方的指标清成 null,於是当对方要被解构的时候就不会去作归 还记忆体的动作。 - copy/move 语意的建构子跟指派运算子都要作自我赋值检查。大家都知道,简单的内建 型别,像是 int,自我赋值(譬如 x = x;)的时候是绝对不会有问题的,所以我们自己 设计的型别,在自我赋值的时候也应该要作到不能出问题。当我们自己手写 code 的时候 ,几乎不太可能发生自我赋值,但是当使用像是 std::sort() 这样的演算法函式库的时 候,却很容易发生。在 C++0x,像 std::sort() 这样的演算法可以用 move 来取代复制 ,但是跟指派运算子同样的潜在危机还是存在的。 现在你可能会很好奇 rvalue reference 以及 move 语意,跟编译器自动产生(隐式宣告 )的建构子还有指派运算子之间的交互作用是什麽。 1. Move 建构子跟 move 指派运算子绝对不会自动产生。 2. 任何一种使用者自订的 copy 建构子跟 move 建构子都会遮蔽预设建构子。 3. 使用者自订的 copy 建构子会遮蔽预设拷贝建构子,但是使用者自订的 move 建构子 不会遮蔽预设的 copy 建构子。 4. 使用这自订的 copy 指派运算子会遮蔽预设的指派运算子,但是使用者自订的 move 指派运算子不会遮蔽预设的指派运算子。 基本上,自动产生建构子跟指派运算子的规则,跟 move 语意没有交互作用,只有一个例 外,就是当你宣告一个 move 建构子的时候,会如同你宣告任何建构子一样,会遮蔽预设 的建构子。 move semantics: moving from lvalues 好,如果你喜欢用 copy 指派运算子来实做 copy 建构子,那很可能现在你也会想要用 move 指派运算子来实做 move 建构子。这样作不是不可以,但是你必须要很小心。像这 样就是一个错误的例子: C:\Temp>type unified_wrong.cpp #include <stddef.h> #include <iostream> #include <ostream> using namespace std; class remote_integer { public: remote_integer() { cout << "Default constructor." << endl; m_p = NULL; } explicit remote_integer(const int n) { cout << "Unary constructor." << endl; m_p = new int(n); } remote_integer(const remote_integer& other) { cout << "Copy constructor." << endl; m_p = NULL; *this = other; } #ifdef MOVABLE remote_integer(remote_integer&& other) { cout << "MOVE CONSTRUCTOR." << endl; m_p = NULL; *this = other; // WRONG } #endif // #ifdef MOVABLE remote_integer& operator=(const remote_integer& other) { cout << "Copy assignment operator." << endl; if (this != &other) { delete m_p; if (other.m_p) { m_p = new int(*other.m_p); } else { m_p = NULL; } } return *this; } #ifdef MOVABLE remote_integer& operator=(remote_integer&& other) { cout << "MOVE ASSIGNMENT OPERATOR." << endl; if (this != &other) { delete m_p; m_p = other.m_p; other.m_p = NULL; } return *this; } #endif // #ifdef MOVABLE ~remote_integer() { cout << "Destructor." << endl; delete m_p; } int get() const { return m_p ? *m_p : 0; } private: int * m_p; }; remote_integer frumple(const int n) { if (n == 1729) { return remote_integer(1729); } remote_integer ret(n * n); return ret; } int main() { remote_integer x = frumple(5); cout << x.get() << endl; remote_integer y = frumple(1729); cout << y.get() << endl; } C:\Temp>cl /EHsc /nologo /W4 /O2 unified_wrong.cpp unified_wrong.cpp C:\Temp>unified_wrong Unary constructor. Copy constructor. Copy assignment operator. Destructor. 25 Unary constructor. 1729 Destructor. Destructor. C:\Temp>cl /EHsc /nologo /W4 /O2 /DMOVABLE unified_wrong.cpp unified_wrong.cpp C:\Temp>unified_wrong Unary constructor. MOVE CONSTRUCTOR. Copy assignment operator. Destructor. 25 Unary constructor. 1729 Destructor. Destructor. (编译器在这边使用了 RVO,没有用上 NRVO。正如我之前所说的,有些拷贝建构子的成 本可以藉由 RVO 或是 NRVO 省下来,但是没有办法全部省下来也是很合逻辑的。剩下的 这些状况就交给 move 建构子来处理。) 上面标注了 WRONG 的那一行,呼叫的是 copy 指派运算子!他正确的通过了编译,也顺 利的执行完毕,但是他就是没有施行 move 语意。 现在是什麽状况?你还记得 C++98/03 里面说过:具名的 lvalue reference 就是 lvalue(当我们写 int& r = *p; 那 r 就是一个 lvalue),不具名的 lvalue reference 还是 lvalue(当 vector<int> v(10, 1729); 时 v[0] 会传回一个 int&,这 就是一个不具名的 lvalue reference,他虽然不具名,你还是可以对他取址)。Rvalue reference 就不一样啦: 1. 具名的 rvalue reference 是 lvalue。 2. 不具名的 rvalue reference 是 rvalue。 一个具名的 rvalue reference 是一个 lvalue,是因为你可以引用(mention)他好几次 ,对他作很多不同的运算。但是如果是一个 rvalue 的话,那只有第一次的运算能碰到这 个他,并且有机会从里面偷东西,後续的运算则完全没有机会。所谓「偷」就是说不能被 发现,所以第一次偷完之後,这个东西就不能再被参用到。另外一方面呢,一个不具名的 rvalue reference 不可能被重复参用,所以他可以保持他 rvalue 的特性。 如果你真的很想用你的 move 指派运算子来实做你的 move 建构子,你需要一项特异功能 :把一个 lvalue 看作是一个 rvalue。C++0x 的 <utility> 里面的 std::move() 提供 你这个能力。VC10 将会有这个功能(其实我自己现在用的开发版已经有了),但是不好 意思现在还是 VC10 CTP,没有。所以我会教你怎麽重头打造一个 move()。 C:\Temp>type unified_right.cpp #include <stddef.h> #include <iostream> #include <ostream> using namespace std; template <typename T> struct RemoveReference { typedef T type; }; template <typename T> struct RemoveReference<T&> { typedef T type; }; template <typename T> struct RemoveReference<T&&> { typedef T type; }; template <typename T> typename RemoveReference<T>::type&& Move(T&& t) { return t; } class remote_integer { public: remote_integer() { cout << "Default constructor." << endl; m_p = NULL; } explicit remote_integer(const int n) { cout << "Unary constructor." << endl; m_p = new int(n); } remote_integer(const remote_integer& other) { cout << "Copy constructor." << endl; m_p = NULL; *this = other; } #ifdef MOVABLE remote_integer(remote_integer&& other) { cout << "MOVE CONSTRUCTOR." << endl; m_p = NULL; *this = Move(other); // RIGHT } #endif // #ifdef MOVABLE remote_integer& operator=(const remote_integer& other) { cout << "Copy assignment operator." << endl; if (this != &other) { delete m_p; if (other.m_p) { m_p = new int(*other.m_p); } else { m_p = NULL; } } return *this; } #ifdef MOVABLE remote_integer& operator=(remote_integer&& other) { cout << "MOVE ASSIGNMENT OPERATOR." << endl; if (this != &other) { delete m_p; m_p = other.m_p; other.m_p = NULL; } return *this; } #endif // #ifdef MOVABLE ~remote_integer() { cout << "Destructor." << endl; delete m_p; } int get() const { return m_p ? *m_p : 0; } private: int * m_p; }; remote_integer frumple(const int n) { if (n == 1729) { return remote_integer(1729); } remote_integer ret(n * n); return ret; } int main() { remote_integer x = frumple(5); cout << x.get() << endl; remote_integer y = frumple(1729); cout << y.get() << endl; } C:\Temp>cl /EHsc /nologo /W4 /O2 /DMOVABLE unified_right.cpp unified_right.cpp C:\Temp>unified_right Unary constructor. MOVE CONSTRUCTOR. MOVE ASSIGNMENT OPERATOR. Destructor. 25 Unary constructor. 1729 Destructor. Destructor. (之後我用到 std::move() 或是我自己的 Move() 的时候,会彼此交替使用,反正他们 在实做上一模一样。)到底 std::move() 背後是怎麽作到这件事情的?此刻我只能跟你 说这是「魔法」。(後面会有一个详尽的解释,其实这东西不复杂,但是牵扯到样板引数 推导跟 reference collapse(译注:一个 reference 的 reference,还是 reference。 C++98/03 的时候,并不允许我们对一个 reference 作 reference)。我们在讲「完美转 发」的时候,还会看到这东西)跳过玄学的部份,我也可以用实际的例子来说明。当我们 有一个 string 的 lvalue,比方说上面示范多载的时候提到的「up」,std::move(up) 会呼叫 string&& std::move(string&) 这个版本,然後传回一个不具名的 rvalue reference,当然,是一个 rvalue。那当我们有一个 string 的 rvalue,比方说上面提 到的 strange(),std::move(strange()) 会呼叫 string&& std::move(string&&) 这个 版本,还是传回一个不具名的 rvalue reference,当然,也是一个 rvalue,所以不管你 塞什麽东西进去,你都会拿到一个 rvalue reference,是 rvalue。 除了让你可以用 move 指派运算子来实做 move 建构子,std::move() 在别的地方也很有 用。不管什麽时候,只要你手上有一个 lvalue,但是你知道他反正马上就要蒙主宠招了 ,你都可以把你的左值运算式加上 std::move() 来启动 move 语意。 move semantics: movable members C++0x 的标准类别(像是 vector,string,或是 regex)都有 move 建构子跟 move 指 派运算子。而且我们已经看到要怎麽帮我们自己设计的类别加上 move 语意(刚刚那个 remote_integer 的范例)。但是如果我们设计的类别里面,有 move 语意的类别怎麽办 (像是 vector,string,或是 regex)?编译器不会自动帮我们产生 move 建构子跟 move 指派运算子,所以我们必需要自己来。但是我们很幸运,有了 std::move(),这件 事情变的超简单。 C:\Temp>type point.cpp #include <stddef.h> #include <iostream> #include <ostream> using namespace std; template <typename T> struct RemoveReference { typedef T type; }; template <typename T> struct RemoveReference<T&> { typedef T type; }; template <typename T> struct RemoveReference<T&&> { typedef T type; }; template <typename T> typename RemoveReference<T>::type&& Move(T&& t) { return t; } class remote_integer { public: remote_integer() { cout << "Default constructor." << endl; m_p = NULL; } explicit remote_integer(const int n) { cout << "Unary constructor." << endl; m_p = new int(n); } remote_integer(const remote_integer& other) { cout << "Copy constructor." << endl; if (other.m_p) { m_p = new int(*other.m_p); } else { m_p = NULL; } } remote_integer(remote_integer&& other) { cout << "MOVE CONSTRUCTOR." << endl; m_p = other.m_p; other.m_p = NULL; } remote_integer& operator=(const remote_integer& other) { cout << "Copy assignment operator." << endl; if (this != &other) { delete m_p; if (other.m_p) { m_p = new int(*other.m_p); } else { m_p = NULL; } } return *this; } remote_integer& operator=(remote_integer&& other) { cout << "MOVE ASSIGNMENT OPERATOR." << endl; if (this != &other) { delete m_p; m_p = other.m_p; other.m_p = NULL; } return *this; } ~remote_integer() { cout << "Destructor." << endl; delete m_p; } int get() const { return m_p ? *m_p : 0; } private: int * m_p; }; class remote_point { public: remote_point(const int x_arg, const int y_arg) : m_x(x_arg), m_y(y_arg) { } remote_point(remote_point&& other) : m_x(Move(other.m_x)), m_y(Move(other.m_y)) { } remote_point& operator=(remote_point&& other) { m_x = Move(other.m_x); m_y = Move(other.m_y); return *this; } int x() const { return m_x.get(); } int y() const { return m_y.get(); } private: remote_integer m_x; remote_integer m_y; }; remote_point five_by_five() { return remote_point(5, 5); } remote_point taxicab(const int n) { if (n == 0) { return remote_point(1, 1728); } remote_point ret(729, 1000); return ret; } int main() { remote_point p = taxicab(43112609); cout << "(" << p.x() << ", " << p.y() << ")" << endl; p = five_by_five(); cout << "(" << p.x() << ", " << p.y() << ")" << endl; } C:\Temp>cl /EHsc /nologo /W4 /O2 point.cpp point.cpp C:\Temp>point Unary constructor. Unary constructor. MOVE CONSTRUCTOR. MOVE CONSTRUCTOR. Destructor. Destructor. (729, 1000) Unary constructor. Unary constructor. MOVE ASSIGNMENT OPERATOR. MOVE ASSIGNMENT OPERATOR. Destructor. Destructor. (5, 5) Destructor. Destructor. 现在你看到啦,资料成员的 move 语意很容易作到。注意 remote_point 的 move 指派运 算子不需要作自我赋值检查,因为 remote_integer 已经检查过了。也注意到 remote_point 预设的拷贝建构子,指派运算子,还有解构子都正常的运作。 你现在应该跟 move 语意已经熟烂了(希望不是你的脑袋炸烂了)。为了测试一下你新获 得的这个能力,你就写一个 remote_integer 的 operator+() 来当作回家作业吧。 最後的叮咛:只要你自己写得类别支援拷贝语意,你都该尽量帮他加上 move 语意的建构 子跟指派运算子,因为编译器不会自动帮你作这件事。因为不是只有你平常写的程式码可 以从 move 语意获利,当你使用 STL 的容器跟演算法的时候,你都可以省下很多的昂贵 的复制成本。 the forwarding problem C++98/03 的 lvalue,rvalue,reference,还有 template 看起来很完美,但是当程式 设计师想要写出高度泛化的函数的时候,就发现有问题了。假设你想要写一个究极的泛型 函数 outer(),这个函数存在的人生目标就是把他拿到的所有的参数转发给另外一个叫做 inner() 的函数,不管这些引数的型别是什麽,数量有多少,我们都希望他能够完美的 做好这件事情。这种行为有很多例子,比方说 factory 函数 make_shard<T>(args),要 把 args 转给 T 的建构子,然後传回一个 shared_ptr<T>。(这样我们就可以把 T 型别 用来管理 reference counting 的程式码统统集中到一个地方,而效能就跟你使用的是侵 入式 reference counting 一样好。)像 function<Ret (Args)> 这样包装类别,把参数 转发给自己内部储存的 functor,也是一个例子。在这篇文章我们只专注在 outer() 函 数把参数转给 inner() 函数的部份。至於 outer() 的传回值那是另外一回事(有的状况 很简单,比方说 make_shared<T>(args) 永远就是传回 shared_ptr<T>,但是如果你想要 将传回值完美的泛型化,就必须要用到 C++0x 的 decltype)。 如果函数没有参数,那这个问题不存在,但是如果是有一个参数的函数呢?让我们试试看 这样设计 outer(): template <typename T> void outer(T& t) { inner(t); } 问题来了,如果参数是一个 non-const rvale,这个 outer() 没办法被呼叫(违反常数 性)。又如果 inner() 接收的是 const int&,那 inner(5) 编译会过,但是 outer(5) 就过不了。因为 5 是一个 int,所以 T 会被推导为 int,但是很可惜你没办法把 int& 系结到 5。 好吧,那不然我们试试看这样: template <typename T> void outer(const T& t) { inner(t); } 如果 inner() 接收的是 int&,那编译根本不会过,因为违反常数性。 现在我们可以试试看多载 outer() 的两个版本,outer(const T& t) 跟 outer(T& t), 这个方法的确有用。当你呼叫 outer() 的时候,就像是你直接呼叫 inner() 一样。 可惜,这个方法在多参数的时候就吃瘪了。以两个参数的例子,你就必须要多载 (T1&, T2&),(const T1&, T2&),(T1&, const T2&),(const T1&, const T2&) 四个版本。当 你的参数一多的时候,你要多载函数数目就会以指数成长。(VC9 的 tr1::bind() 光作 前五个参数就已经让人对这个世界绝望透顶,有 63 个多载函数。但是我们不做的话,我 们就很难跟使用者解释说为什麽连 f(1729) 这样的东西编译都不会过。为了生出这些多 载函数,我们用了非常恶心的 preprocessor 机制,恶心到你连听都不会想听,真的。) 在 C++98/03,转发是很严重的问题,而且本质上无解(用上那些恶心的 preprocessor 技巧又让编译明显变慢,而且那程式码几乎不是人看的)。总算,rvalue reference 优 雅的解决了这个问题。 (我刚刚是先解释多载决议跟 move 语意的观念,然後才讲范例程式。现在我要反过来, 我们要先看怎麽用 rvalue reference 作到完美转发的范例程式,然後我才会说明参数推 导跟 reference collapsing 的规则,因为这样会比较容易懂) perfect forwarding: the pattern 完美转发让你可以只写一个函数就转发所有的引数,不管你有几个引数,也不管这些引数 是什麽型别。引数的 const/non-const 跟 lvalue/rvalue 特性都会被完整的保留,让你 的 outer() 跟你的 inner() 用起一模一样,跟 move 语意一起用的话就更棒啦。( C++0x 的「variadic template」解决了「任意型别数目」的问题,所以我们要做的事情 可以推广到任意多个型别引数。)乍看之下很神奇,实际上很简单: C:\Temp>type perfect.cpp #include <iostream> #include <ostream> using namespace std; template <typename T> struct Identity { typedef T type; }; template <typename T> T&& Forward(typename Identity<T>::type&& t) { return t; } void inner(int&, int&) { cout << "inner(int&, int&)" << endl; } void inner(int&, const int&) { cout << "inner(int&, const int&)" << endl; } void inner(const int&, int&) { cout << "inner(const int&, int&)" << endl; } void inner(const int&, const int&) { cout << "inner(const int&, const int&)" << endl; } template <typename T1, typename T2> void outer(T1&& t1, T2&& t2) { inner(Forward<T1>(t1), Forward<T2>(t2)); } int main() { int a = 1; const int b = 2; cout << "Directly calling inner()." << endl; inner(a, a); inner(b, b); inner(3, 3); inner(a, b); inner(b, a); inner(a, 3); inner(3, a); inner(b, 3); inner(3, b); cout << endl << "Calling outer()." << endl; outer(a, a); outer(b, b); outer(3, 3); outer(a, b); outer(b, a); outer(a, 3); outer(3, a); outer(b, 3); outer(3, b); } C:\Temp>cl /EHsc /nologo /W4 perfect.cpp perfect.cpp C:\Temp>perfect Directly calling inner(). inner(int&, int&) inner(const int&, const int&) inner(const int&, const int&) inner(int&, const int&) inner(const int&, int&) inner(int&, const int&) inner(const int&, int&) inner(const int&, const int&) inner(const int&, const int&) Calling outer(). inner(int&, int&) inner(const int&, const int&) inner(const int&, const int&) inner(int&, const int&) inner(const int&, int&) inner(int&, const int&) inner(const int&, int&) inner(const int&, const int&) inner(const int&, const int&) 两行!只用了两行就作到完美转发!太妙了! 这个例子示范了怎麽把 t1 跟 t2 透明的转发给 inner(),inner() 可以知道 t1 跟 t2 的 lvalue/rvalue 跟 const/non-const 特性,就好像他是直接被呼叫一样。 跟 std::move() 一样,std::identity() 跟 std::forward() 都在 C++0x 的 <utility> 里面定义(VC10 会有,不过 CTP 没有)。我正在示范怎麽实做这两个东西 。(一样的,我会把 std::identity() 还有 std::forward() 跟我自己的 Identity() 还有 Forward() 混用,反正他们是一模一样的东西。) 现在让我们看看这被後到底是干甚麽吃的。其实他靠的就是样板引数推导跟 reference collapse 这两个东西。 rvalue references: template argument deduction and reference collapsing Rvalue reference 跟样板是以很特别的方式互动,这边是一个范例。 C:\Temp>type collapse.cpp #include <iostream> #include <ostream> #include <string> using namespace std; template <typename T> struct Name; template <> struct Name<string> { static const char * get() { return "string"; } }; template <> struct Name<const string> { static const char * get() { return "const string"; } }; template <> struct Name<string&> { static const char * get() { return "string&"; } }; template <> struct Name<const string&> { static const char * get() { return "const string&"; } }; template <> struct Name<string&&> { static const char * get() { return "string&&"; } }; template <> struct Name<const string&&> { static const char * get() { return "const string&&"; } }; template <typename T> void quark(T&& t) { cout << "t: " << t << endl; cout << "T: " << Name<T>::get() << endl; cout << "T&&: " << Name<T&&>::get() << endl; cout << endl; } string strange() { return "strange()"; } const string charm() { return "charm()"; } int main() { string up("up"); const string down("down"); quark(up); quark(down); quark(strange()); quark(charm()); } C:\Temp>cl /EHsc /nologo /W4 collapse.cpp collapse.cpp C:\Temp>collapse t: up T: string& T&&: string& t: down T: const string& T&&: const string& t: strange() T: string T&&: string&& t: charm() T: const string T&&: const string&& (译注:这边讲一下「参数(parameter)」跟「引数(argument)」的差别。参数指的是你 宣告函数的时候,写在参数串列的东西,比方说 int min(int a, int b); 的 a 跟 b 就 是参数。引数指的是你在呼叫这个函数的时候,实际传给函数的那个东西,比方说 int c = min(4, 7); 的 4 跟 7 就是引数。很多人有时候会用参数来指引数,其实这篇文章 的原文里面也会这样,我翻译的时候也会这样,看状况决定使用哪一个名词。但是接下来 的部份因为要仔细的区分这两者,原文就写得很小心,所以我也翻得很小心,当我说「参 数」的时候就是很明确的「parameter」,不会是「argument」,当我说「引数」的时候 就是「parameter」,那你们也要看得很小心,不然会觉得很奇怪 yoco 不知道在说三小 。) 藉由显示指定 Name 的型别来印出我们 T 的型别。 当呼叫 quark(up) 的时候,会进行型别引数推导。quark() 有一个型别参数,但是我们 没有显示指定他的型别(比方像 quark<X>(up))。所以 up 就会被拿来和宣告 quark() 时所使用的 T&& 进行比较,以进行型别引数推导。 C++0x 会把函数的参数跟引数都转型,来进行比对动作。 首先会先转换函数引数的型别。有一条特殊规则(N2798 14.8.2.1 [demp.deduct.call]/3):「when the function parameter type is of the form T&& where T is a template parameter, and the function argument is an lvalue of type A, the type A& is used for template argument deduction.」(当参数型别是 T&&,且引数是一个型别 A 的 lvalue 的时候,引数型别会推导为 A&。)(但是这条特 殊规则在引数型别是 T& 或是 const T& 的时候不会作用,这样跟 C++98/03 的行为一样 ,另外在引数型别是 const T&& 的时候也不会作用。)在 quark(up) 这个例子,我们会 把 string 转成 string&。 然後会转换函数参数的型别。不管是 C++98/03 还是 C++0x 都一样,会卸除 reference (不管是 lvalue 还是 rvalue 的 reference 在 C++0x 都会被卸除掉)在这个例子呢, 代表 T&& 会变成 T。 於是 T 就是你传进来的引数的型别,这就是为什麽,quark(up) 会印出「T: string&」 然後 quark(down) 会印出「T: const string&」。up 跟 down 都是 lvalue,所以他们 会启动那个特殊规则。strange() 跟 charm() 是 rvalue,所以他们用的就是本来旧有的 规则,所以 quark(strange()) 印出「T: string」然後 quark(charm()) 印出「T: const string」。 做完引数型别推导之後,会进行替换动作。每一个样板引数 T 都会被替换成他经过推导 之後的型别。以 quark(strange()) 来说,T 是 string,所以 T&& 就会是 string&&。 同样的,quark(charm()) 时,T 是 const string,所以 T&& 就会是 const string&&。 但是 up 跟 down 不是这样,他们的情况有另外一条特殊规则来处理。 看看 quark(up),T 是 string&,T&& 经过替换以後就变成 string& &&,在C++0x 里面 ,reference 的 reference 会被折叠(reference collapse),折叠的规则是:「 lvalue reference 有传染性」。X& &,X& && 还有 X&& & 都会变成 X&,只有 X&& && 会变成 X&&。所以 string& && 会被折叠成 string&。所以说在 template 的世界里面, 看起来像是 rvalue reference 的东西其实不一定是 rvalue reference。 Name<T&&>::get() 就是一个例子。同样的,quark(down) 会具现化 quark<const string&>(),因为 T&& 会被替换成 string&。在 C++98/03,你可能已经用过常数性来遮 蔽样板参数的参数型别(比方说一个样板函数接受一个 T& 当作参数,但是当你传一个 const F 物件给他的时候,T* 就会是 const Foo*)。在 C++0x,lvalue 的特性也会遮 蔽样板的参数型别。 好,那这两条特殊规则对我们有什麽影响?在 quark() 的参数列我们使用 T&&,T&& 会 跟 quark() 接到的引数有一模一样的型别,包含了 lvalue/rvalue 还有 const/non-const 的所有特性都全部保留了。这就是为什麽可以用 rvalue reference 来作到完美转发。 perfect forwarding: how std::forward() and std::identity work 回头再看一次 outer()。 template <typename T1, typename T2> void outer(T1&& t1, T2&& t2) { inner(Forward<T1>(t1), Forward<T2>(t2)); } 现在我们知道为什麽 outer() 要收 T1&& 跟 T2&& 了,因为这样可以保留完整的型别资 讯。但是为什麽要呼叫 Forward<T1>() 跟 Forward<T2>()?回忆一下,不管是具名的 lvalue reference 还是具名的 rvalue reference 都是 lvalue。如果 outer() 直接呼 叫 inner(t1, t2),那 inner() 就会拿到两个 lvalue,这样就打断了完美转发。 幸运啦,不具名的 lvalue reference 是 lvalue,然後不具名 rvalue reference 是 rvalue。所以为了要把 t1 跟 t2 转发给 inner(),我们必需要透过一个函数来帮忙保 留他们的型别,但是可以去掉他们的名字。这就是 std::forward() 的功用。 template <typename T> struct Identity { typedef T type; }; template <typename T> T&& Forward(typename Identity<T>::type&& t) { return t; } 当呼叫 Forward<T1>(t1) 的时候,Identify 并没有改变 T1(我们马上就会看到他是干 甚麽吃的)。所以 Forward<T1>(t1) 吃了一个 T1&& 又吐了一个 T1&&。这样 t1 的型别 就保持不变(不管他吃什麽东西都一样,string&,const string&,string&&,const string&& 都一样),但是名字不见啦!inner() 只会看到 Forward<T1>(t1),而这东西 跟 t1 有一模一样的型别,不管 lvalue/rvalue,const/non-const 都会跟 outer() 当 初拿到的一模一样。完美转发就是这样。 你大概好奇如果你不小心写了 forward<T1&&>(t1) 会怎样。(这个错误还蛮诱人的,因 为 outer() 接的是 T1&& t1。)很幸运,没什麽不好的事情会发生。Forward<T1&&>() 会拿到一个 T1&& &&,并且传回一个 T1&& &&,然後会被折叠成 T1&&。因此, Forward<T1>(t1) 跟 Forward<T1&&>(t1) 是一模一样,但是我们偏好前面那一个,因为 程式码比较短。 那 Identity 是干啥吃的?为什麽不能直接这样写? template <typename T> T&& Forward(T&& t) { // BROKEN return t; } 如果这样写,那就是隐式呼叫。这样样板引数推导就会作用,我们已经知道引数推导会干 嘛了,当你传进来的值是一个 lvalue 的时候,他会把 T&& 改成 T&,也就是 lvalue。 但是我们一直想要解决的问题,不就是 outer() 里面不管你收到的是 lvalue 还是 rvalue 结果都会是一个 lvalue 吗。在上面那个错误实做范例,Forward<T1>(t1) 会对 ,但是 Forward(t1) 就错啦,他会直接把 t1 的型别传给 inner(),这是一个非常诱惑 人的陷阱,因为他会毫发无伤的通过编译,然後会带来一连串的苦难,所以 Identity 是 用来阻止样板引数型别的自动推导。在 Identity<T>::type 里面的那两个冒号是一层绝 缘体,样板引数型别推导的作用没办法穿越他,很有经验的程式设计师应该对这些东西很 熟,因为这点不管在 C++98/03 还是 C++0x 都一样。(不过详细的原因又是另外一件事 了。) move semantics: how std::move() works 现在会了样板引数推导的特殊规则以後,让我们回头看 std::move(): template <typename T> struct RemoveReference { typedef T type; }; template <typename T> struct RemoveReference<T&> { typedef T type; }; template <typename T> struct RemoveReference<T&&> { typedef T type; }; template <typename T> typename RemoveReference<T>::type&& Move(T&& t) { return t; } RemoveReference 的机制跟 C++0x 的 <type_traits> 里面的std::remove_reference 完 全一样。举例来说 RemoveReference<string>::type, RemoveReference<string&>::type 跟 RemoveReference<string&&>::type 都一样是 string。 同样的,Move() 跟 C++0x <utility> 的 std::move() 一模一样。 1. 当呼叫 Move(string),string 是一个 lvalue 的时候,T 会被推导成 string&,所 以 Move() 会拿到 string&,经过 RemoveReference 处理完会变成 string,最後会传回 一个 string&&。 2. 当呼叫 Move(string),string 是一个 const lvalue 的时候,T 会被推导成 const string&,所以 Move() 拿到 string& 传给 RemoveReference 之後,会传回一个 const string&&。 3. 当呼叫 Move(string),string 是一个 rvalue 的时候,T 会被推导成 string, Move() 拿到一个 string&&,传回一个 string&&。 4. 当呼叫 Move(string),string 是一个 const rvalue 的时候,T 会被推导成 const string,Move() 拿到一个 const string&&,传回一个 const string&&。 这就是 Move() 之所以可以保留型别跟常数性,但是可以把 lvalue 变成 rvalue 的原理 。 the past 如果想要对 rvalue referece 了解的更多,可以看他们的提案文件。不过要提一下的是 那些提案所写的东西跟现在决定的可能已经不一样了。Rvalue reference 已经被整合到 C++0x 标准的工作文件,但是很多提案文件也许已经过时了,或是不太对,或是没有被 采纳,但是如论如何那些提案还是有很多有用的资讯。 N1377,N1385,还有 N1690 是主要的提案。N2118 含括了在提入标准前的最後版本。 N1784,N1821,N2377,还有 N2439 是「Extending Move Semantics To *this(把 move 语意扩充到 *this)」的演变过程,这已经被纳入 C++0x 了,但是 VC10 还没有 实做。 the future N2812「A safety Problem With Rvalue Reference (and what to do about it)」提出 了一个初始化规则的变动,可以防止 rvalue reference 被系结到 lvalue 上面。里面提 到这项改变并不会影响 move 语意跟完美转发,所以你现在学到的东西都还是有用(但是 std::move() 跟 std::forward() 的实做会改变)。 Stephan T. Lavavej Visual C++ Libraries Developer -- To iterate is human, to recurse is divine. 递回只应天上有, 凡人该当用回圈.   L. Peter Deutsch --



※ 发信站: 批踢踢实业坊(ptt.cc)
◆ From: 118.160.109.51
1F:推 james732:先推再看 03/02 01:59
2F:推 legendmtg:yoco大大辛苦了<(_ _)> 03/02 02:02
3F:推 WalkingIce:(狂笑)...推! 03/02 02:06
4F:推 ctrlbreak: 推再看 03/02 02:19
5F:推 Killercat:XDDDD推一下 03/02 02:19
6F:推 plover:XD 03/02 02:30
7F:推 LPH66:先推再看 XD 03/02 02:37
8F:推 VictorTom:BBS版还是有颜色啊XD 总之先推再看....:) 03/02 03:09
9F:推 saxontai:先推,有空再看 XD 03/02 03:57
10F:推 UNARYvvv:推 好久没注意C/C++了.. 03/02 04:34
11F:推 H45:有够长 03/02 05:21
12F:推 wa120:推 03/02 07:23
13F:推 wangm4a1:推 03/02 08:09
14F:推 redray:Push 03/02 08:35
15F:推 legnaleurc:推~~ 03/02 08:52
16F:推 zlw:真的很长,谢谢 03/02 09:15
17F:推 redluna:先推再看XD 03/02 09:31
18F:推 akasan:先推再看!C++真的很多豆知识XD 03/02 09:34
19F:推 windows2k:先推再看 03/02 09:37
20F:推 johnlinvc:推 03/02 10:28
21F:推 avhacker:其实就是 MOJO: Move of join objects 03/02 10:40
22F:→ avhacker:参考这篇 http://tinyurl.com/bvcdvw 03/02 10:40
23F:→ yoco315:其实不是 mojo 喔.. 完全不一样.. mojo 是 Andrie 在 03/02 12:31
24F:→ yoco315:C++98/03 给的 move 语意 solution, 缺点是他是侵入式的 03/02 12:32
25F:→ yoco315:rvalue reference 是语言层面的东西,而且解决了完美转发 03/02 12:32
26F:推 avhacker:嗯,我讲的不够精确,应该说这是解决 mojo 想解决的事 03/02 12:43
27F:推 ADF:推~ 03/02 13:52
28F:推 Holocaust123:酷唷 03/02 17:53
29F:推 etime:推一下 03/02 19:21
30F:推 shrimpp:推 03/02 22:57
31F:推 ledia:本想看完再推... 还是先推一下好了 XD 03/02 23:31
32F:推 kkc:推~ 03/03 03:38
33F:推 mizuki2005:这麽有心 当然要推... 03/03 10:22
34F:推 weiyucsie:推荐这篇文章 XD 03/03 12:23
35F:推 godman362:先推再看XD 03/03 17:36
36F:推 jyhfang:真厉害 XD 03/03 21:10
※ 编辑: yoco315 来自: 118.160.108.48 (03/04 03:26)
37F:推 aecho:推~~ 03/04 19:10
38F:推 bgcrwf:大推~~ 03/04 20:21
39F:推 DemonRing:push... 03/06 01:43
40F:推 hrs113355:推 03/07 00:37
41F:推 Ovaltine1015:好物一定要来推一下!! ^^ 03/19 01:15
42F:推 fetosa:推! 08/04 12:59
43F:推 lachu:推XD 08/17 20:21
44F:推 ko1:先推再看 XD 09/14 18:28
45F:推 loking:推~~谢谢分享 09/29 01:08
46F:推 wanwan2:推推~~ 04/08 13:50
※ 编辑: yoco315 来自: 118.160.112.83 (09/07 22:26)
47F:推 spider391:大推~~!! 12/15 16:30
48F:推 prime2477:推 05/01 10:46
49F:推 almod:推 07/04 06:11
50F:推 Arton0306:有看有推 10/19 11:06
51F:推 wch6858:推 02/02 11:38
52F:推 FAITHY:推!!! 02/03 00:55
53F:推 xxxx9659:有看有推!!! 03/29 07:52
54F:推 xx52002:有看有推 04/04 04:37
55F:推 Feis:推~ 09/10 02:01
56F:推 ACMANIAC: 朝圣,这太猛了吧 12/26 07:40
57F:推 apple50189: 推推 05/09 20:14







like.gif 您可能会有兴趣的文章
icon.png[问题/行为] 猫晚上进房间会不会有憋尿问题
icon.pngRe: [闲聊] 选了错误的女孩成为魔法少女 XDDDDDDDDDD
icon.png[正妹] 瑞典 一张
icon.png[心得] EMS高领长版毛衣.墨小楼MC1002
icon.png[分享] 丹龙隔热纸GE55+33+22
icon.png[问题] 清洗洗衣机
icon.png[寻物] 窗台下的空间
icon.png[闲聊] 双极の女神1 木魔爵
icon.png[售车] 新竹 1997 march 1297cc 白色 四门
icon.png[讨论] 能从照片感受到摄影者心情吗
icon.png[狂贺] 贺贺贺贺 贺!岛村卯月!总选举NO.1
icon.png[难过] 羡慕白皮肤的女生
icon.png阅读文章
icon.png[黑特]
icon.png[问题] SBK S1安装於安全帽位置
icon.png[分享] 旧woo100绝版开箱!!
icon.pngRe: [无言] 关於小包卫生纸
icon.png[开箱] E5-2683V3 RX480Strix 快睿C1 简单测试
icon.png[心得] 苍の海贼龙 地狱 执行者16PT
icon.png[售车] 1999年Virage iO 1.8EXi
icon.png[心得] 挑战33 LV10 狮子座pt solo
icon.png[闲聊] 手把手教你不被桶之新手主购教学
icon.png[分享] Civic Type R 量产版官方照无预警流出
icon.png[售车] Golf 4 2.0 银色 自排
icon.png[出售] Graco提篮汽座(有底座)2000元诚可议
icon.png[问题] 请问补牙材质掉了还能再补吗?(台中半年内
icon.png[问题] 44th 单曲 生写竟然都给重复的啊啊!
icon.png[心得] 华南红卡/icash 核卡
icon.png[问题] 拔牙矫正这样正常吗
icon.png[赠送] 老莫高业 初业 102年版
icon.png[情报] 三大行动支付 本季掀战火
icon.png[宝宝] 博客来Amos水蜡笔5/1特价五折
icon.pngRe: [心得] 新鲜人一些面试分享
icon.png[心得] 苍の海贼龙 地狱 麒麟25PT
icon.pngRe: [闲聊] (君の名は。雷慎入) 君名二创漫画翻译
icon.pngRe: [闲聊] OGN中场影片:失踪人口局 (英文字幕)
icon.png[问题] 台湾大哥大4G讯号差
icon.png[出售] [全国]全新千寻侘草LED灯, 水草

请输入看板名称,例如:BabyMother站内搜寻

TOP