完整原文链接:https://codinfox.github.io/dev/2014/06/03/move-semantic-perfect-forward/
移动语义(move semantic)
通过移动语义,我们可以在没有必要的时候避免复制。那么在接下来,我们就重点来谈一谈移动构造函数(move constructor)。相信到这里你已经意识到了,移动构造函数的出现就是为了解决复制构造函数的这个弊病。所以,其实移动构造函数应该和复制构造函数实现差不多的功能。那么,它也应该是一种构造函数的重载(好废的废话……)。所以,我们可以想象出来,其实移动构造函数大概就会是这个样子:
Test(<KEYWORD> t):arr(t.arr){t.arr = nullptr;}
这里解释一下,通过移动构造函数,事实上我们是做了一个浅拷贝(shallow copy)。至于要将之前的指针置为空的原因在于,我们的类会在析构的时候delete掉我们的数组。那么我们浅拷贝出来的这个对象的成员变量(arr
指针)就变成了一个悬挂指针(dangling pointer)。
好了,现在的问题变成了,这个<KEYWORD>
究竟是什么?编译器如何自动判断到底应该调用复制构造函数(我突然想起来这个东西的翻译貌似应该是拷贝构造函数,但是既然都已经写了这么多了,我就不改了)还是移动构造函数呢?
....................
进一步探讨左值和右值
我们来考虑下面的情景:
void doWork(TYPE&& param) {
// ops and expressions using std::move(param)
}
这个代码是从Scott Meyers的演讲当中摘取的。现在的问题是:** param
是右值吗? **答案是:不!param
是一个左值。
这里牵扯到一个概念,即事实上左值和右值与类型是没有关系的,即int
既可以是左值,也可以是右值。区别左值和右值的唯一方法就是其定义,即能否取到地址。在这里,我们明显可以对param
进行取地址操作,所以它是一个左值。也就是说,但凡有名字的“右值”,其实都是左值。这也就是为什么上面的代码当中鼓励大家对所有的变量使用std::move()
转成右值的原因。
....................
完美转发(perfect forward)又是在做什么
我们依然考虑一个例子:
template <typename T>
void func(T t) {
cout << "in func" << endl;
}
template <typename T>
void relay(T&& t) {
cout << "in relay" << endl;
func(t);
}
int main() {
relay(Test());
}
在这个例子当中,我们的期待是,我们在main
当中调用relay
,Test
的临时对象作为一个右值传入relay
,在relay
当中又被转发给了func
,那这时候转发给func
的参数t
也应当是一个右值。也就是说,我们希望:当relay
的参数是右值的时候,func
的参数也是右值;当relay
的参数是左值的时候,func
的参数也是左值。
那么现在我们来运行一下这个程序,我们会看到,结果与我们预想的似乎并不相同:
default constructor
in relay
copy constructor
in func
destructor
destructor
我们看到,在relay
当中转发的时候,调用了复制构造函数,也就是说编译器认为这个参数t
并不是一个右值,而是左值。这个的原因已经在上一节将结果了,因为它有一个名字。那么如果我们想要实现我们所说的,如果传进来的参数是一个左值,则将它作为左值转发给下一个函数;如果它是右值,则将其作为右值转发给下一个函数,我们应该怎么做呢?
这时,我们需要std::forward<T>()
。与std::move()
相区别的是,move()
会无条件的将一个参数转换成右值,而forward()
则会保留参数的左右值类型。所以我们的代码应该是这样:
template <typename T>
void func(T t) {
cout << "in func " << endl;
}
template <typename T>
void relay(T&& t) {
cout << "in relay " << endl;
func(std::forward<T>(t));
}
现在运行的结果就成为了:
default constructor
in relay
move constructor
in func
destructor
destructor
而如果我们的调用方法变成:
int main() {
Test t;
relay(t);
}
那么输出就会变成:
default constructor
in relay
copy constructor
in func
destructor
destructor
完美地实现了我们所要的转发效果。
.............
................
后记
C++0x通过引入许多新的语言特性来实现了语言性能的提升,使得本来就博大精深的一门语言变得更加的难以学习。但是一旦了解,就会被语言精妙的设计所折服。参考资料中给出了更多的关于左值、右值、左值引用、右值引用、移动语义和完美转发的例子。我自己实在是没有精力看完所有的这些资料了,各位有兴趣的话可以参阅。
参考资料
- http://thbecker.net/articles/rvalue_references/section_01.html#section_01
- http://blog.csdn.net/pongba/article/details/1697636
- http://channel9.msdn.com/Shows/Going+Deep/Cpp-and-Beyond-2012-Scott-Meyers-Universal-References-in-Cpp11
- https://isocpp.org/blog/2012/11/universal-references-in-c11-scott-meyers
- https://onedrive.live.com/view.aspx?resid=F1B8FF18A2AEC5C5!1062