转自:VC10中的C++0x特性 Part 2 :右值引用
简介
这一系列文章介绍Microsoft Visual Studio 2010 中支持的C++ 0x特性,目前有三部分。
Part 1:介绍了Lambdas, 赋予新意义的auto,以及 static_assert;
Part 2:介绍了右值引用(Rvalue References);
Part 3:介绍了表达式类型(decltype)。
今天我要讲的是 rvalue references (右值引用),它能实现两件不同的事情: move 语意和完美转发。刚开始会觉得它们难以理解,因为需要区分 lvalues 和 rvalues ,而只有极少数 C++98/03 程序员对此非常熟悉。这篇文章会很长,因为我打算极其详尽地解释 rvalue references 的运作机制。
不用害怕,使用 ravlue references 是很容易的,比听起来要容易得多。要在你的代码中实现 move semantics 或 perfect forwarding 只需遵循简单的模式,后文我会对此作演示的。学习如何使用 rvalue references 是绝对值得的,因为 move semantics 能带来巨大的性能提升,而 perfect forwarding 让高度泛型代码的编写变得非常容易。
C++ 98/03 中的 lvalues 和 rvalues
要理解C++ 0x中的 rvalue references,你得先理解 C++ 98/03 中的 lvalues 与 rvalues.
术语 “lvalues” 和 “rvalues” 是很容易被搞混的,因为它们的历史渊源也是混淆。(顺带一提,它们的发音是 ‘L values“ 和 ”R values“, 尽管它们都写成一个单词)。这两个概念起初来自 C,后来在 C++ 中被加以发挥。为节省时间,我跳过了有关它们的历史,比如为什么它们被称作 ”lvalues“ 和 ”rvalues“,我将直接讲它们在 C++ 98/03 中是如何运作的。(好吧,这不是什么大秘密: ”L“ 代表 ”left“,”R“ 代表 ”right“。它们的含义一直在演化而名字却没变,现在已经”名“不副”实“了。与其帮你上一整堂历史课,不如随意地把它们当作像”上夸克“和”下夸克“之类的名字,也不会有什么损失。)
C++ 03 标准 3.10/1 节上说: “每一个表达式要么是一个 lvalue ,要么就是一个 rvalue .” 应该谨记 lvalue 跟 rvalue 是针对表达式而言的,而不是对象。
lvalue 是指那些单一表达式结束之后依然存在的持久对象。例如: obj,*ptr, prt[index], ++x 都是 lvalue.
rvalue 是指那些表达式结束时(在分号处)就不复存在了的临时对象。例如: 1729 , x + y , std::string("meow") , 和 x++ 都是 rvalue.
注意 ++x 和 x++ 的区别。当我们写 int x = 0; 时, x 是一个 lvalue,因为它代表一个持久对象。 表达式 ++x 也是一个 lvalue,它修改了 x 的值,但还是代表原来那个持久对象。然而,表达式 x++ 却是一个 rvalue,它只是拷贝一份持久对象的初值,再修改持久对象的值,最后返回那份拷贝,那份拷贝是临时对象。 ++x 和 x++ 都递增了 x,但 ++x 返回持久对象本身,而 x++ 返回临时拷贝。这就是为什么 ++x 之所以是一个 lvalue,而 x++ 是一个 rvalue. lvalue 与 rvalue 之分不在于表达式做了什么,而在于表达式代表了什么(持久对象或临时产物)。
另一个培养判断一个表达式是不是 lvalue 的直觉感的方法就是自问一下“我能不能对表达式取址?”,如果能够,那就是一个 lvalue;如果不能,那就是 一个 rvalue. 例如:&obj , &*ptr , &ptr[index] , 和 &++x 都是合法的(即使其中一些例子很蠢),而 &1729 , &(x + y) , &std::string("meow") , 和 &x++ 是不合法的。为什么这个方法凑效?因为取址操作要求它的“操作数必须是一个 lvalue”(见 C++ 03 5.3.1/2)。为什么要有那样的规定?因为对一个持久对象取址是没问题的,但对一个临时对象取址是极端危险的,因为临时对象很快就会被销毁(译注:就像你有一个指向某个对象的指针,那个对象被释放了,但你还在使用那个指针,鬼知道这时候指针指向的是什么东西)。
前面的例子不考虑操作符重载的情况,它只是普通的函数调用语义。“一个函数调用是一个 lvalue 当且仅当它返回一个引用”(见 C++ 03 5.2.2/10)。因此,给定语句 vercor<int> v(10, 1729); , v[0] 是一个 lvalue,因为操作符 []() 返回 int& (且 &v[0] 是合法可用的); 而给定语句 string s("foo");和 string t("bar");,s + t 是一个rvalue,因为操作符 +() 返回 string(而 &(s + t) 也不合法)。
lvalue 和 rvalue 两者都有非常量(modifiable,也就是说non-const)与常量(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& 可绑定到非常量 lvalue (可以用这个引用来读取和修改原来的值),但不能绑定到 const lvalue,因为那将违背 const 正确性;也不能把它绑定到非常量 rvalue,这样做极端危险,你用这个引用来修改临时对象,但临时对象早就不存在了,这将导致难以捕捉而令人讨厌的 bug,因此 C++ 明智地禁止这这么做。(我要补充一句:VC 有一个邪恶的扩展允许这么蛮干,但如果你编译的时候加上参数 /W4 ,编译器通常会提示警告"邪恶的扩展被激活了“)。也不能把它绑定到 const ravlue,因为那会是双倍的糟糕。(细心的读者应该注意到了我在这里并没有谈及模板参数推导)。
const Type& 可以绑定到: 非常量 lvalues, const lvalues,非常量 rvalues 以及 const values.(然后你就可以用这个引用来观察它们)
引用是具名的,因此一个绑定到 rvalue 的引用,它本身是一个 lvalue(没错!是 L)。(因为只有 const 引用可以绑定到 rvalue,所以它是一个 const lvalue)。这让人费解,(不弄清楚的话)到后面会更难以理解,因此我将进一步解释。给定函数 void observe(const string& str), 在 observe()'s 的实现中, str 是一个 const lvalue,在 observe() 返回之前可以对它取址并使用那个地址。这一点即使我们通过传一个 rvalue 参数来调用 observe()也是成立的 ,就像上面的 three() 和 four()。也可以调用 observe("purr"),它构建一个临时 string 并将 str 绑定到那个临时 string.three() 和 foure() 的返回对象是不具名的,因此他们是 rvalue,但是在 observe()中,str 是具名的,所以它是一个 lvalue.正如前面我说的“ lvalue 跟 rvalue 是针对表达式而言的,而不是对象”。当然,因为 str 可以被绑定到一个很快会被销毁的临时对象,所以在 observe() 返回之后我们就不应该在任何地方保存这个临时对象的地址。
你有没有对一个绑定到 rvalue 的 const 引用取址过么?当然,你有过!每当你写一个带自赋值检查的拷贝赋值操作符: Foo& operator=(const Foo& other), if( this != &other) { copy struff;}; 或从一个临时变量来拷贝赋值,像: Foo make_foo(); Foo f; f = make_foo(); 的时候,你就做了这样的事情。
这个时候,你可能会问“那么非常量 rvalues 跟 const rvalues 有什么不同呢?我不能将 Type& 绑定到非常量 rvalue 上,也不能通过赋值等操作来修改 rvalue,那我真的可以修改它们?” 问的很好!在 C++ 98/03 中,这两者存在一些细微的差异: non-constrvalues 可以调用 non-const 成员函数。 C++ 不希望你意外地修改临时对象,但直接在non-const rvalues上调用 non-const 成员函数,这样做是很明显的,所以这是被允许的。在 C++ 0x中,答案有了显著的变化,它能用来实现 move 语意。
恭喜!你已经具备了我所谓的“lvalue/rvalue 观”,这样你就能够一眼就判断出一个表达式到底是 lvalue 还是 rvalue.再加上你原来对 const 的认识,你就能完全理解为什么给定语句 void mutate(string& ref) 以及前面的变量定义, mutate(one) 是合法的,而 mutate(two), mutate(three()), mutate(four()), mutate("purr") 都是不合法的。如果你是 C++ 98/03 程序员,你已经可以分辨出这些调用中的哪些是合法的,哪些是不合法的;是你的“本能直觉”,而不是你的编译器,告诉你 mutate(three()) 是假冒的。你对 lvalue/rvalue 的新认识让你明确地理解为什么 three() 是一个 rvalue,也知道为什么非常量引用不能绑定到右值。知道这些有用么?对语言律师而言,有用,但对普通程序员来说并不见得。毕竟,你如果不理解关于 lvalues 和 rvalues 一切就要领悟这个还隔得远呢。但是重点来了:与 C++ 98/03 相比, C++ 0x 中的 lvalue 和 rvalue 有着更广泛更强劲的含义(尤其是判断表达式是否是 modifiable / const 的 lvalue/rvalue,并据此做些处理)。要有效地使用 C++ 0x,你也需具备对 lvalue/rvalue 的理解。现在万事具备,我们能继续前行了。
拷贝的问题
C++ 98/03 将不可思议的高度抽象和不可思议的高效执行结合到了一起,但有个问题:它过度滥用拷贝。对行为像 int 那样有着值语意的对象而言,源对象的拷贝是独立存在的,并不会影响源对象。值语意很好,除了在会导致冗余拷贝之外,像拷贝 strings,vectors 等重型对象那样的情况。(“重型”意味着“昂贵的拷贝开销”;有着100万个元素的 vector 是重型的)。返回值优化(RVO) 和命名返回值优化(NRVO)在特定情况下可以优化掉拷贝构造操作,这有助于减缓问题的严重性,但是它们不能够消除所有冗余的拷贝。
最最没有必要的拷贝是拷贝那些立马会被销毁的对象。你有过复印一份文件,并马上把原件扔掉的经历么(假定原件和复件是相同的)?那简直是浪费,你应该持有原件而不必费劲去复印。下面是被我称作“杀手级的示例”,来自标准委员会的例子(见提案 N1377),假设你有一大堆 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;
这样做的效率如何?(我们不用为这个特殊的例子而担忧,它的执行只要几微秒;我们担忧它的一般化情况,在语言层面上的情况)。
每次调用操作符 +() 就会返回一个临时 string.上面调用了 8 次操作符 +(),因而产生了 8 个临时 string. 每一个临时 string,在构造过程中分配动态内存,再拷贝所有已连接的字符,最后在析构过程中释放分配的动态内存。(你听说过短串优化技术么,为了避免动态内存的分配与释放,VC是这么干的,在这个被我精心挑选的有着合适长度的 s0 面前短串优化技术也无能为力,即使执行了这样的优化,也无法避免拷贝操作。如果你还听说过写时拷贝优化(Copy - On - Write),忘了它吧,在这里也不适用,并且在多线程环境下这种优化会恶化问题,因此标准库实现根本就不再做这个优化了)。
事实上,因为每一个串接操作都会拷贝所有已经串接好的字符,所以那个复杂度是字符串长度的平方了。哎呀!这太浪费了!这点确实让 C++ 尴尬。事情怎么会搞成这样呢?有没有改善的办法?
问题是这样的,operator+()接受两个参数,一个是 const string&,另一个是 const string& 或 const char * (还有其他重载版本,但在这里我们没有用到),但 operator+() 无法分辨出你塞给它的是 lvalue 还是 rvalue 参数,所以它只好总是创建一个临时 string,并返回这个临时 string. 为什么这跟 vavlue/rvalue 有关系?
当我们要计算 s0 + " " 的值时,很明显这里有必要创建一个新的临时 string. s0 是一个 lvalue,它已经命名了一个持久对象,因此我们不能修改它。(有人注意到了!) .如果要计算 (s0 + “ ”) + s1 的值,我们可以简单地将 s1 的内容追加到第一个临时 string 上,而不用创建第二个临时 string 再把第一个丢弃掉。这就是 move 语意背后的核心观念: 因为 s0 + " " 是一个 rvalue ,只有那个在整个程序中唯一能够觉察到临时对象存在的表达式可以引用临时对象。如果我们能检测到表达式是一个非常量 rvalue,我们就可以任意修改临时对象,而不会有人发现。 操作符 +() 本不应该修改它的参数,但如果其参数是非常量 rvalue,谁在乎?照这种方法,每次调用操作符 +() 都把字符追加到唯一的临时对象上,这样就彻底省掉了不必要的动态内存管理和冗余的拷贝操作,呈现出线性复杂度。耶!
从技术上讲,在 C++ 0x 中,每次调用操作符 +() 还是会返回一个单独的临时 string. 然而,第二个临时 string (产生自 (s0 + “ ”) + s1 )可以通过“窃取”第一个临时 string (产生自 s0 + " " )的内存而被构造出来,然后再把 s1 的内容追加到那块内存后面(这将会引发一个普通的重分配操作)。“窃取”是通过指针的操作实现的:第二个临时 string 会先拷贝第一个临时 string 的内部指针,然后再清空这个指针。第一个临时 string 最后被销毁(在分号那地方)时,它的指针已经置为 null 了,因此它的析构函数什么也不会做(译注:也就是说不会释放它的内存,这部分内存现在是第二个临时 string 在使用了)。
通常,如果能够检测到非常量 rvalue,你就能够做些“资源窃取”的优化。如果非常量 rvalue 所引用的那些对象持有任何资源(如内存),你就能窃取它们的资源而不用拷贝它们,反正它们很快就会被销毁掉。通过窃取非常量 rvalue 持有的资源来构建或赋值的手法通常被称作 “moving”,可移动对象拥有 “move 语意”。
在大多数情况下这相当有用,比如 vector 的重新分配。当一个 vector 需要更多空间(如 push_back() 时)和进行重分配操作时,它需要从旧的内存块中拷贝元素到新的内存块中去。这些拷贝构造调用的开销很大。(对 vector<string> 来说,需要拷贝每一个 string 元素,这涉及动态内存分配)。但是等一等!旧内存块中的那些元素很快会被销毁掉的呀,所以我们可以挪动这些元素,而不用拷贝它们。在这种情形下,旧内存块中的元素依然存在于内存中,用来访问它们的表达式,如 old_ptr[index],还是 lvalue.在重分配过程中,我们想用非常量 rvalue 表达式来引用旧内存块中的元素。假定它们是非常量 rvalue,那我们就能够移动它们,从而省去拷贝构造开销。(说“我想假定这个 lvalue 是一个非常量 rvalue ”等同于说“我知道这是一个 lvalue,它指向一个持久对象,但我不关心随后会对这个 lvalue 进行怎样的操作,或销毁它,或给它赋值,或进行任意操作。因此如果你能从它那里窃取资源的话,尽管行动吧”)
C++0x 的 rvalue 引用概念给与我们检测非常量 rvalue 并从中窃取资源的能力,这让我能够实现 move 语意。rvalue 引用也让我们能够通过把 lvalue 伪装成非常量 rvalue 而随意触发 move 语意。现在,我们来看看 rvalue 引用是如何工作的!
ravlue 引用:初始化
C++0x 引进了一种新的引用,ravlue 引用,其语法是 Type&& 和 const Type&& .目前 C++0x 草案 N2798 8.3.2/2 上说:“用 & 声明的引用类型被称作 lvalue 引用,而用 && 声明的引用类型被称作 rvalue 引用。lvalue 引用与 rvalue 引用是截然不同的类型。除非特别注明,两者在语意上是相当的并且一般都被称作引用。”这意味着对 C++98/03 中引用(即现在的 lvalue 引用)的直觉印象可以延伸用于 rvalue 引用;你只需要学习这两者的不同之处。
(说明:我选择把 Type& 读作 “Type ref”,Type&& 读作 "Type ref ref".它们的全称分别是 “lvalue reference to Type” 和 "rvalue reference to Type",就像 “cosnt pointer to int” 被写成 “int * const”,而被读作 “int star const”一样。)
两者有什么区别?与 lvalue 引用相比, rvalue 引用在初始化与重载决议时表现出不同的行为。两者的区别在于它们会优先绑定到什么东西上(初始化时)和什么东西会优先绑定到它们身上(重载决议时)。首先让我们来看看初始化:
我们已经明白为何非常量 lvalue 引用( Type& ) 只能绑定到非常量 lvalue 上,而其他的一概不能(如 const lvalues,非常量 rvalues,const rvalues)
· 我们已经明白为何 const lvalue 引用( const Type& ) 能绑定到任何东西上。
· 非常量 rvalue ( Type&& ) 能够绑定到非常量 lvalue 以及非常量 rvalue 上,而不能绑定到 const lvalues 和 const rvalues (这会违背 const 正确性)
· const rvalue 引用( const Type&& ) 能够绑定到任何东西上。
这些规则听起来可能有些神秘,但是他们来源于两条简单的规则:
· 遵守 const 正确性,所以你不能把非常量引用绑定到常量上。
· 避免意外修改临时对象,所以你不能把非常量 lvalue 引用绑定到非常量 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 |
非常量 rvalue 引用绑定到非常量 rvalue 是没问题的;要领就是它们可以被用来修改临时对象。
虽然 lvalue 引用和 rvalue 引用在初始化时有着相似的行为(只有第 18 和 28 行不同),但在重载决议的时候它们的区别就很显著了。
rvalue 引用:重载决议
函数可根据非常量和常量 lvalue 引用参数的不同而重载,这一点你应该很熟悉了。在 C++0x 中,函数也可根据非常量和常量 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() |
在实践中,全部重载 Type& , const Type& , Type&& , const Type&& 并不是很有用。只重载 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 引用,rvalue 最优先绑定到 rvalue 引用。
(3) 非常量表达式倾向于绑定到非常量引用上。
(我说的“否决权”是指:进行重载决议时初始化规则否决那些不可行(译注:不满足 const 正确性)的候选函数,这些函数阻止将表达式绑定到引用上) 让我们一条一条来看看这些规则是怎么运作的。
·对 purr(up) 而言,决议(1)初始化规则既不否决 purr(const string&) 也不否决 purr(string&&)。 up 是 lvalue,因此满足决议(2)中的 lvalue 最优先绑定到 lvalue 引用,即 purr(const string&)。up 还是非常量,因此满足决议(3)非常量表达式倾向于绑定到非常量引用上,即purr(string&&)。两者放一块决议时,决议(2)胜出,选择 purr(const string&)。
·对 purr(down) 而言, 决议(1)初始化规则基于 const 正确性否决掉 purr(string&&),因此 purr(const string&) 胜出。
·对 purr(strange()) 而言,决议(1)初始化规则既不否决 purr(const string&) 也不否决 purr(string&&)。strange() 是 rvalue, 因此满足决议(2) rvalue 最优先绑定到 rvalue 引用,即 purr(string&&)。strange() 还是非常量,因此满足决议(3)非常量表达式倾向于绑定到非常量引用上,即purr(string&&)上。purr(string&&) 在这里两票胜出。
·对 purr(charm()) 而言,决议(1)初始化规则基于 const 正确性否决掉 purr(string&&),因此 purr(const string&) 胜出。
值得注意的是当你只重载了const Type& 和 Type&& ,非常量 rvalue 绑定到 Type&&,而其它的都绑定到 const Type&.因此,这一组重载用来实现 move 语义。
重要说明:返回值的函数应当返回 Type(如 strange() )而不是返回 const Type (如 charm())。后者不会带来什么好处(阻止非常量成员函数调用),还会阻止 move 语意优化。
move 语义:模式
下面是一个简单的类 remote_integer, 内部存储一个指向动态分配的 int 指针(“远程拥有权”)。你应该对这个类的默认构造函数,一元构造函数,拷贝构造函数,拷贝赋值函数和析构函数都很熟悉了。我给它增加了 move 构造函数和 move 赋值函数,它们被#ifdef 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. |
·我们重载了拷贝构造函数和 move 构造函数,还重载了拷贝赋值函数和 move 赋值函数。在前面我们已经看到了当函数通过 const Type& 和 Type&& 进行重载时,会有怎样的结果。当 move 语意可用时,b = square(a) 会自动选择调用 move 赋值函数。
·move 构造函数和 move 赋值函数只是简单的从 other 那里“窃取”内存,而不用动态分配内存。当“窃取”内存时,我们只是拷贝 other 的指针成员,然后再把它置为 null.于是当 other 被销毁时,析构函数什么也不做。
·拷贝赋值函数和 move 赋值函数都需要进行自我赋值检查,为何拷贝赋值函数需要进行自我赋值检查是广为人知的。这是因为像 int 这样的内建数据(POD)类型能够正确地自我赋值(如:x = x ),因此,用户自定义的数据类型理应也可以正确地自我赋值。自我赋值实际上在手写代码里面是不存在的,但是在类似 std::sort() 之类的算法中,却很常见。在 C++0x 中,像 std::sort() 之类的算法能够通过挪动而非拷贝元素来实现。在这里(move 赋值函数)也需要进行自我赋值检查。
这时,你可能会想它们( move 拷贝构造函数和 move 赋值函数)与编译器自动生成(标准中用词“隐式声明”)的默认拷贝构造函数和默认赋值函数有什么相互影响呢。
·永远不会自动生成 move 构造函数和 move 赋值函数。
·用户声明的构造函数,拷贝构造函数和 move 构造函数会抑制住默认构造函数的自动生成。
·用户声明的拷贝构造函数会抑制住默认拷贝构造函数的自动生成,但是用户声明的 move 构造函数做不到。
·用户声明的拷贝赋值函数会抑制住默认拷贝赋值函数的自动生成,但是用户声明的 move 赋值函数做不到。
基本上,除了声明 move 构造函数会抑制默认构造函数的自动生成以外,自动生成规则不影响 move 语义。
move 语意:从 lvalue 移动
现在,如果你喜欢用拷贝赋值函数来实现你的拷贝构造函数该怎样做呢,那你也可能试图用 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 构造函数来优化。)
move 构造函数中标记为 WRONG 的那一行,调用了拷贝赋值函数,编译能通过也能运行,但这违背了 move 构造函数的本意。(译注:因为那个拷贝赋值函数只是进行普通的拷贝赋值,而不是 move 赋值!)
这是怎么回事呢?记住:在C++98/03中,具名 lvalue 引用是左值(给定语句 int& r = *p; r 是 lvalue),不具名 lvalue 引用还是左值(给定语句 vector<int> v(10, 1729), v[0] 返回 int&, 你可以对这个不具名 lvalue 引用取址)。但是 rvalue 引用就不一样了:
· 具名 lvalue 引用是 lvalue.
· 不具名 rvalue 引用是 rvalue.
一个具名 rvalue 引用是一个 lvalue 是因为可以对它施加多重操作,重复使用。相反,如果它是一个 ravlue 的话,那么对它施加的第一个操作能够“窃取”它,而后续操作就没机会了。这里的“窃取”是说不会被察觉到,所以这是行不通的。另一方面,不具名 rvalue 引用不能被重复使用,所以它仍保持右值(rvalueness)语意。
如果你真的打算用 move 赋值函数来实现 move 构造函数,你需要从 lvalue move,就像是从 rvalue move 一样。C++0x <utility> 中的 std::move() 具备这样的能力,VC10将会有这个(实际上,开发版中已经有了),但VC10 TCP版还没有,所以我会教你从头做起:
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 collapsing,译注:引用的引用)有 关,后面讲完美转发的时候我们还会遇到这两个东西)。我可以用一个具体的例子来略过“魔法”:给定一个 string 类型的左值,像前面重载决议例子中的 up ,std::move(up) 调用 string&& std::move(string&),这个函数返回一个不具名的 rvalue 引用,它是一个 rvalue.给定一个 string 类型的 rvalue,像前面重载决议例子中的 strange(), std::move(strange()) 调用 string&& std::move(string&&),同样这个函数还是返回一个不具名的 rvalue,还是 rvalue.
std::move() 除了让你能用 move 复制函数来实现 move 构造函数之外,还能在其他地方发挥作用。无论何时,只要你有一个左值,而它的值也不再重要了(例如,它将被销毁或被赋值),你就可以使用 std::move(你的左值表达式) 来使用 move 语意。
move 语意:可移动成员(movable member)
C++0x 的标准类型(像 vector, string, regex) 都有 move 构造函数和 move 赋值函数。而且我们也已经看到了如何在我们自己的类中通过手动管理资源来实现 move 语意(像前面的 remote_integer 类)。如果类中包含可移动数据成员(像 vector, string, regex, remote_integer )时该怎么办呢?编译器不会自动帮我们自动产生 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. |
现在你看到啦,按成员移动(memberwise move)是很容易做到的。注意, remote_point 的 move 赋值函数没有进行自我赋值检查,是因为 remote_integer 已经检查过了。也要注意到 remote_point 隐式声明的拷贝构造函数,拷贝赋值函数和析构函数都正常运作。
到现在,你应该对 move 语意已经非常熟悉了。(希望不是抓狂啊!)为了测试你新获得的这个不可思议的技能,请为前面的例子写一个 +() 操作符函数当作练习吧。
最后的提醒:只要你的类支持 move 语意,你就应该实现 move 构造函数和 move 赋值函数。因为不仅仅是你平常使用这些类时可从 move 语意中获利, STL 容器和算法也能从中获利,通过廉价的 move 省下昂贵的拷贝开销。