Welcome

首页 / 软件开发 / VC.NET / VC10中的C++0x特性 Part 2 (1):右值引用

VC10中的C++0x特性 Part 2 (1):右值引用2010-05-29 vcblog Stephan T. Lavavej本文为 Part 2 的第一页

今天我要讲的是 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 的理解。现在万事具备,我们能继续前行了。