`
luliangok
  • 浏览: 778644 次
文章分类
社区版块
存档分类
最新评论

【译】VC10中的C++0x特性 Part 2 (2):右值引用

 
阅读更多

【译】VC10中的C++0x特性 Part 2 (2):右值引用

来源:vcblog 翻译:飘飘白云 kesalin@gmail.com

(转载时请注明作者和出处。未经许可,请勿用于商业用途)


简介

这一系列文章介绍Microsoft Visual Studio 2010 中支持的C++ 0x特性,目前有三部分。
Part 1 :介绍了Lambdas, 赋予新意义的auto,以及 static_assert;
Part 2( , ):介绍了右值引用(Rvalue References);
Part 3 :介绍了表达式类型(decltype)

VC10中的C++0x特性 Part 1,2,3 译文打包下载(doc 和 pdf 格式): 点此下载

本文是 Part 2 的第二页。


<!-- [if gte mso 9]><xml> <w:WordDocument> <w:View>Normal</w:View> <w:Zoom>0</w:Zoom> <w:PunctuationKerning /> <w:DisplayHorizontalDrawingGridEvery>0</w:DisplayHorizontalDrawingGridEvery> <w:DisplayVerticalDrawingGridEvery>2</w:DisplayVerticalDrawingGridEvery> <w:ValidateAgainstSchemas /> <w:SaveIfXMLInvalid>false</w:SaveIfXMLInvalid> <w:IgnoreMixedContent>false</w:IgnoreMixedContent> <w:AlwaysShowPlaceholderText>false</w:AlwaysShowPlaceholderText> <w:Compatibility> <w:SpaceForUL /> <w:BalanceSingleByteDoubleByteWidth /> <w:DoNotLeaveBackslashAlone /> <w:ULTrailSpace /> <w:DoNotExpandShiftReturn /> <w:AdjustLineHeightInTable /> <w:BreakWrappedTables /> <w:SnapToGridInCell /> <w:WrapTextWithPunct /> <w:UseAsianBreakRules /> <w:DontGrowAutofit /> <w:UseFELayout /> </w:Compatibility> <w:BrowserLevel>MicrosoftInternetExplorer4</w:BrowserLevel> </w:WordDocument> </xml><![endif]--><!-- [if gte mso 9]><xml> <w:LatentStyles DefLockedState="false" LatentStyleCount="156"> </w:LatentStyles> </xml><![endif]--><!-- [if !mso]> <object classid="clsid:38481807-CA0E-42D2-BF39-B33AF135CC4D" id=ieooui> </object> <mce:style><! st1/:*{behavior:url(#ieooui) } --><!-- [endif]--><!-- [if gte mso 10]> <mce:style><! /* Style Definitions */ table.MsoNormalTable {mso-style-name:普通表格; mso-tstyle-rowband-size:0; mso-tstyle-colband-size:0; mso-style-noshow:yes; mso-style-parent:""; mso-padding-alt:0mm 5.4pt 0mm 5.4pt; mso-para-margin:0mm; mso-para-margin-bottom:.0001pt; mso-pagination:widow-orphan; font-size:10.0pt; font-family:Century; mso-ansi-language:#0400; mso-fareast-language:#0400; mso-bidi-language:#0400;} --><!-- [endif]-->

<!-- [if gte mso 9]><xml> <w:WordDocument> <w:View>Normal</w:View> <w:Zoom>0</w:Zoom> <w:PunctuationKerning /> <w:DrawingGridVerticalSpacing>7.8 磅</w:DrawingGridVerticalSpacing> <w:DisplayHorizontalDrawingGridEvery>0</w:DisplayHorizontalDrawingGridEvery> <w:DisplayVerticalDrawingGridEvery>2</w:DisplayVerticalDrawingGridEvery> <w:ValidateAgainstSchemas /> <w:SaveIfXMLInvalid>false</w:SaveIfXMLInvalid> <w:IgnoreMixedContent>false</w:IgnoreMixedContent> <w:AlwaysShowPlaceholderText>false</w:AlwaysShowPlaceholderText> <w:Compatibility> <w:SpaceForUL /> <w:BalanceSingleByteDoubleByteWidth /> <w:DoNotLeaveBackslashAlone /> <w:ULTrailSpace /> <w:DoNotExpandShiftReturn /> <w:AdjustLineHeightInTable /> <w:BreakWrappedTables /> <w:SnapToGridInCell /> <w:WrapTextWithPunct /> <w:UseAsianBreakRules /> <w:DontGrowAutofit /> <w:UseFELayout /> </w:Compatibility> <w:BrowserLevel>MicrosoftInternetExplorer4</w:BrowserLevel> </w:WordDocument> </xml><![endif]--><!-- [if gte mso 9]><xml> <w:LatentStyles DefLockedState="false" LatentStyleCount="156"> </w:LatentStyles> </xml><![endif]--><!-- [if !mso]> <object classid="clsid:38481807-CA0E-42D2-BF39-B33AF135CC4D" id=ieooui> </object> <mce:style><! st1/:*{behavior:url(#ieooui) } --><!-- [endif]--><!-- [if gte mso 10]> <mce:style><! /* Style Definitions */ table.MsoNormalTable {mso-style-name:普通表格; mso-tstyle-rowband-size:0; mso-tstyle-colband-size:0; mso-style-noshow:yes; mso-style-parent:""; mso-padding-alt:0cm 5.4pt 0cm 5.4pt; mso-para-margin:0cm; mso-para-margin-bottom:.0001pt; mso-pagination:widow-orphan; font-size:10.0pt; font-family:"Times New Roman"; mso-fareast-font-family:"Times New Roman"; mso-ansi-language:#0400; mso-fareast-language:#0400; mso-bidi-language:#0400;} --><!-- [endif]-->

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 省下昂贵的拷贝开销。

转发问题

在程序员不用写高度泛化的代码的时候, C++98/03 lvalue rvalue 引用,还有模板看起来是很完美的。假设你要写一个完全泛化的函数 outer() ,这个函数的目的是将任意数目个任意类型的参数传递(也就是 转发 )给函数 inner() 。已有很多不错的解决方案,比如 factory 函数 make_shared<T>(args) 是把 args 传给 T 的构造函数,然后返回 shared_ptr<T> 。(这样就把 T 对象和用于对它进行引用计数的代码存储到同一块动态内存中,性能上与侵入式引用计数一样好); 而像 function<Ret(args)> 这样的包装类是把参数传给其内部存储的函数对象( functor ),等等。在这篇文章里,我们只对 outer() 是如何把参数传递给 inner() 这部分感兴趣。至于 outer() 的返回类型是怎么决定的是另外的问题(有时候很简单,如 make_shared<T>(args) 总是返回 shared_prt<T> ,),但要在完全搞定这个问题的一般化情况,你就要用到 C++0x decltype 特性了)。

如果不带参数,就不存在这样的问题,那么带一个参数情况呢?让我们尝试写个 outer()

template <typename T> void outer(T& t) {

inner(t);

}

题来了,如果传给它的参数是非常量 rvalue ,那我们就无法调用 outer() 。如果 inner() 接收 const int& 型的参数,那 inner(5) 是可以通过编译的,但是 outer(5) 就编译不过了。 因为 T 会被推导为 int int& 是不能绑定到常量 5 的。

好吧,让我们试试这个:

template <typename T> void outer(const T& t) {

inner(t);

}

如果 inner () 接收 int& 型参数,那就会违法 const 正确性,编译都过不了。

现在,你可以重载两个分别带 T& const T& 参数的 outer () ,这确实管用。 当你调用 outer() 时,就像直接调用 inner() 一样。

惜的是,这中方法在多参数的情况下就麻烦了(译注:要写的重载函数太多了)。你就得为每一个参数像 T1& const T1&, T2& const T2& 等这样进行重载,要重载的函数数目呈指数级增长。( VC9 SP1 tr1 :: bind() 就够让人感到绝望了,它为 5 个参数这么重载出了 63 个函数。如果不这么蛮干的话,没有像这里的长篇累述,我们就很难跟使用者解释为什么不能调用用 1729 这样的 ravlue 做参数的函数。为了产生出这些重载函数使用了令人作呕的预处理机制,恶心到你都不想知道它)。

C++98/03 中,转发问题是很严重的,而且本质上无解(必须求助于恶心的预处理机制,这会严重拖慢编译速度,还让代码变得难以阅读)。总算, rvalue 优雅地解决了这个问题。

完美 转发 模式

完美转发让你能简单而清晰地只写一个模板函数就可以转发所有的参数给任意函数,不管它带几个参数,也不管参数类型是什么。而且参数的非常量 / 常量, lvalue/rvalue 属性都能得以保留,让你可以像使用 inner() 一样使用 outer() ,还可以和 move 语意一起用从而获得额外的好处。( C++0x 的变长模板技术解决了 任意数目 这部分,我们在这里把 N 看做任意数目)。乍看之下很神奇,实际上很简单:

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 outer () 透明地转发给 inner () inner () 可以知道它们的非常量 / 常量, lvalue/ravlue 属性,就像 inner 是被直接调用的那样。

std :: move() 一样, std::identify std::forward() 都是在 C++<utility> 中定义的( VC10 会有, VC10 CTP 中没有)。我将演示怎么来实现它们。(再次,我将交替使用 std::identity 和我的 Identity std::forward() 和我的 Forward() ,因为他们的实现是等价的。)

现在,让我们来揭开 魔术 的神秘面纱,其实它靠的就是模板参数推导和引用折叠 (reference collapsing) 技术。

rvalue 引用:模板参数推导和引用折叠 (reference collapsing)

rvalue 引用与模板以一种特别的方式相互作用。下面是一个示例:

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&&

这里藉由 Name 的显式规格说明来打印出类型。

我们调用 quark(up) 时,会进行模板参数推导。 quark() 是一个带有模板参数 T 的模板函数,但是我们还没有为它提供显式的类型参数(比如像 quark<X>(up) 这样的)。通过比较函数形参类型 Type&& 和函数实参类型(一个 string 类型的 lvalue )我们就能推导出模板实参类型。(译注:原文用 argument 表示实参, parameter 表示形参)

C++0x 会转换函数实参的类型和形参的类型,然后再进行匹配。

首先,转换函数实参的类型。这遵循一条特殊规则 ( 提案 N2798 14.8.2.1[temp.deduct.call]/3) :如果模板形参类型为 T ,函数形参类型为 T&& ,且函数实参类型为 A lvalue ,那么模板实参类型会被推导为 A& 。(但这条特殊规则不适用于函数形参类型为 T& const T& 的情况,这种情况下会和 C++98/03 保持一致,另外它也不适用于函数形参类型为 const T&& 的情况)。在 quark(up) 这个例子中,依照这个规则我们会把 string 转换成 string&

然后,转换函数形参的类型。不管是 C++98/03 还是 C++0x 都会解除引用 ( lvalue 引用和 rvalue 引用在 C++0x 中都会被解除掉 ) 。在前面例子的四种情形中,这样我们会把 T&& 转换成 T

于是, T 会被推导成函数实参转换之后的类型 up down 都是 lvalue ,它们遵循那条特殊规则,这就是为什么 quark(up) 打印出 "T:string& " ,而 quark(down) 打印出 "T: cosnt string& " 的原因。 strange() charm() 都是右值,它们遵循一般规则,这就是为什么 quark(strange()) 打印出 "T: string " quark(charm()) 打印出 "T: const string " 的原因。

替换操作会在类型推导之后进行。模板形参 T 出现的每一个地方都会被替换成推导出来的模板实参类型。在 quark(string()) T string ,因此 T&& 会是 string&& 。同样,在 quark(charm()) 中, T const string 因此 T&& const string&& 。但 quark(up) quark(down) 不同,它们遵循另外的特殊规则。

quark(up) 中, T string& 。进行替换的话 T&& 就成了 string& && ,在 C++0x 中会折叠( collapse )引用的引用,引用折叠的规则就是 “lvalue 引用是传染性的 X& & , X& && X&& & 都会被折叠成 X& ,只有 X&& && 会被折叠成 X&& 。因此 string& && 被折叠成 string& 。在模板世界里,那些看起来像 rvalue 引用的东西并不一定真的就是。 因而 quark(up) 被实例化为 quark<string&>() ,进而 T&& 经替换与折叠之后变成 string& 。我们可以调用 Name<T&&>::get() 来验证这个。 同样, quark(down) 被实例化为 quark<const string&>() ,进而 T&& 经替换与折叠之后变成 const string& 。在 C++98/03 中,你可能习惯了常量性 (constness) 隐藏于模板形参中 ( 也就是说可以传 const Foo 对象作实参来调用形参为 T& 的模板函数,就像 T& 会是 const Foo& 一样 ) ,在 C++0x 中,左值属性 (lvalueness) 也能隐藏于模板形参中。

那好,这两条特殊规则对我们有什么影响?在 quark() 内部,类型 T&& 有着和传给 quark() 的实参一样的左 / 右值属性 (lvalueness/rvalueness) 和常量性。这样 rvalue 引用就能保持住左右值属性和常量性,做到完美转发。

完美转发: std::forward() std::identidy 是怎样工作的

让我们再来看看 outer() :

template <typename T1, typename T2> void outer(T1&& t1, T2&& t2) {

inner(Forward<T1>( t1) , Forward<T2>( t2) );

}

现在我们明白了为什么 outer() 的形参是 T1&& T2&& 类型的了,因为它们能够保持住传给 outer() 的实参的信息。那为什么这里要调用 Forward<T1>() Forward<T2>() 呢?还记得么,具名 lvalue 引用和具名 rvalue 引用都是 lvalue 。如果 outer() 调用 inner(t1, t2) ,那么 inner() 总是会当 lvalue 来引用 t1 t2 ,这就破坏了完美转发。

幸运的是,不具名 lvalue 引用是 lvalue ,不具名 rvalue 引用还是 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) Identidy 并没有修改 T1 (很快我们讲到 Identidy T1 做了什么)。因此 Forward<T1>() 接收 T1&& ,返回 T1&& 。这样就移除了 t1 的名字,保持住 t1 的类型信息(而不论 t1 是什么类型, string& 也好 , const string& 也好 , string&& 也好或 const string&& 也好)。这样 inner() 看到的 Forward<T1>(t1) ,与 outer() 接收的第一个实参有着相同的信息,包括类型, lvalueness/rvalueness ,常量性等等。完美转发就是这样工作的。

你可能会好奇如果不小心写成 Forward<T1&&>(t1) 又会怎样呢?(这个错误还是蛮诱人的,因为 outer() 接收的就是 T1&& t1 )。很幸运,没什么坏事情会发生。 Forward<T1&&>() 接收与返回的都是 T1&& && ,这会被折叠成 T1&& 。于是, Forward<T1>(t1) Forward<T1&&>(t1) 是等价的,我们更偏好前者,是因为它要短些。

Identidy 是做什么用的呢?为什么下面的代码不能工作?

template <typename T> T&& Forward(T&& t ) { // BROKEN

return t;

}

如果 Forward() 像是上面那样,它就能被隐式调用(不带明确的模板参数)。 当我们传给 Forward() 一个 lvalue 实参时,模板参数推导就介入了,如我们前面看到的那样会将 T&& 变成 T& ,也就是变成一个 lvalue 引用。问题来了,即使形参 T1&& T2&& 指明是 rvalue 引用,但在 outer() 中,具名的 t1 t2 却是 lvaue ,这个问题是我们一直想要解决的!使用上面那个错误的实现, Forward<T1>(t1) 是可以工作的,而 Foarward(t1) 虽然能通过编译(很诱人哦)但会出错,就如它就是 t1 一样。真是痛苦的源泉啊,因此, Identity 被用来阻止模板参数推导 typename Identity<T>::type 中的那对冒号就像绝缘体,模板参数推导无法穿越它,有模板编程经验的程序员应该对此很熟悉了,因为这在 C++98/03 C++0x 中是一样的。(要解释这个是另外的事情了)

move 语意: std::move() 是怎样工作的

现在我们已经学习了模板参数推导和引用折叠的特殊规则,让我们再来看看 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()

· 当调用 Move(string) , string 是一个 lvalue 时, T 会被推导为 string& ,于是 Move() 接收的就是 string& ( 经折叠之后 ) 并返回 string&& ( RemoveReference 之后 )

· 当调用 Move(const string) , const string 是一个 lvalue 时, T 会被推导为 const string& ,于是 Move() 接收的就是 const string&& ( 经折叠之后 ) 并返回 const string&& ( RemoveReference 之后 )

· 当调用 Move(string) , string 是一个 rvalue 时, T 会被推导为 string ,于是 Move() 接收的就是 string&& 并返回 string&&

· 当调用 Move(const string) , const string 是一个 rvalue 时, T 会被推导为 const string ,于是 Move() 接收的就是 const string&& 并返回 const string&&

这就是 Move() 如何保持其参数的类型和常量性,还能把 lvalue 转换成 rvalue 的过程。

回顾

如果你想对 rvalue 引用有更多了 解,你可以去读有关它们的提案。要注意,提案与现在的决定可能已经不同了, rvalue 引用已经被整合到 C++0x 草案中来了,在那里它得到持续的改进。有些提案或已不再正确,或已过时,或已有了替代方案,就没有被采纳。无论怎样,它们还是能提供一些有用信息的。

N1377 , N1385 , N1690 是主要的提案, N2118 包含被整合进标准草案之前的最后版本。 N1784 , N1821 , N2377 , N2439 记录了 Move 语意扩展到 *this ” 的演化过程,这个也被整合到 C++0x 中来了,但还没有在 VC10 中得到实现。

展望

N2812 “Rvalue 引用的安全问题(以及如何解决) 提出了对初始化规则的修改,它禁止 rvalue 引用绑定到 lvalue 这不会影响 move 语意和完美转发,所以它不会让你刚学到的新技术失效(它只是修改了 std::move() std::forward() 的实现)。

Stephan T. Lavavej

Visual C++ Libraries Developer

Published Tuesday, February 03, 2009 9:27 AM by vcblog

翻译: 飘飘白云

( 转载时请注明作者和出处。未经许可,请勿用于商业用途 )

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics