VC10中的C++0x特性 Part 2 (2) : 右值引用
来源:vcblog 翻译:飘飘白云 kesalin@gmail.com
这个系列的第一部分( 1, 2, 3 )介绍了 lambda, auto 和 static_assert。
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 语义。
标签:C++0x,