来自Herb Sutter's blog的消息,http://herbsutter.com/2011/08/12/we-have-an-international-standard-c0x-is-unanimously-approved/
身为一名自封的伪C++ Geek,虽然水平和经验还差得远,但等到了这一天还是忍不住要说道说道的~
新标准带来了很多新东西,可分为“新的语言特性”和“新的标准库”两部分,前者包含右值引用、lambda、适应并发环境的内存模型等等,后者从Boost库中吸取了很多卓越的成果。今天就来说说个人认为最有意义的新特性:右值引用。
闲话少说,先直接上一段代码:
#include <vector>
#include <ctime>
#include <iostream>
using namespace std;
const int LEN = 200000;
vector<int> makeVec()
{
vector<int> v(LEN);
for(int j=0; j<LEN; ++j){
v[j] = j;
}
return v;
}
int main()
{
clock_t t0 = clock();
vector<vector<int> > vec;
for(int i=0; i<1000; ++i){
vec.push_back(makeVec());
}
cout << (clock() - t0) * 1.0 / CLOCKS_PER_SEC << endl;
return 0;
}
运行环境:T7500, 2G, Win7(32bit)
编译器:GCC4.5.2(Mingw)
编译参数:
g++ -o test0.exe main.cpp
g++ -std=c++0x -o testx.exe main.cpp
运行test0.exe三次,输出分别结果为:4.066; 4.056; 4.041;
运行testx.exe三次,输出结果分别为:2.392; 2.383; 2.415;
g++ -O2 -o test02.exe main.cpp
g++ -O2 -std=c++0x -o testx2.exe main.cpp
运行test02.exe三次,输出分别结果为:2.500; 2.500; 4.041;
运行testx2.exe三次,输出结果分别为:0.740; 0.767; 0.786;
启用新标准编译出的程序效率,无论开优化与否,效率都得到了显著的提升,而代码是同一份,我们没有做任何修改,这就是我个人所谓“新的免费午餐”,呵呵。
这份午餐是谁的杰作?右值引用!右值引用的严肃讲解请看这里:http://blogs.msdn.com/b/vcblog/archive/2009/02/03/rvalue-references-c-0x-features-in-vc10-part-2.aspx
下面是我个人对于右值引用的不严肃讲解:
1. 什么是右值?
用一句不科学也不严谨的话概括:右值是一种表达式的类型,当其表达的变量或对象所在的语句结束时,变量的生命期也随之结束。
例如"string str="hello"; string str1 = str + str;" 第二句等号后边的"str + str"就是一个右值表达式,他所表达的那个字符串对象在这句话结束的时候就会析构。(这种表达式经常出现在等号右边,不知叫他做右值有没有这个原因)
2. 什么是右值引用?
3. 右值引用有什么用?在C++11之前,我们把一个右值传递给某函数的时候,只能绑定到值类型的参数(T、const T)或者常量引用参数(const T&),也就是说,函数对这个传进来的对象本身不能做任何变更性的操作。
而在C++11中是允许将传递给函数的右值绑定到右值引用参数(T&&)上去的,且优先级比常量引用参数高,也就是说,若传进来的这个对象是右值表达的,我们可以对其进行变更性操作了!
八个字:移动语义,资源窃取。
比如我想复制一个右值表达式表示的string对象,我们叫它rstr,我们不必开辟一块新的内存,再把rstr背后的字符数组内容拷贝过来,而是利用“右值表达式所表示的对象生命期快到了”这一条件,直接将rstr背后的那个char*指针变量的值拷贝过来,然后再将这个指针变量的值改为NULL(题外话,C++11中有了新的空指针表示法nullptr),这样rstr在析构的时候不会释放字符串所对应的空间,是不是很美好?这就是所谓的移动语义、资源窃取哈。
而上面代码的例子也是一个道理,makeVec()函数返回的是右值表达式表达的一个vector<int>,我们再把它push_back给一个vector<vector<int>>时,老标准下的编译器就会发生vector<int>背后那个int*所指向区域的复制行为,而新标准下的编译器就会避免复制这片区域,而只是简单的将int*这个指针变量的值“偷”过来。
有这样一个“经验之谈”:“标准库容器中尽量要放内置类型或者对象的指针”。因为标准库容器是值语义的,即push_back()操作是复制一份”参数所表达的对象“放到容器中去的。我们想要把一个对象放入容器需要把它先造出来,然后再复制一份到容器中去,所以为了避免不必要的复制,我们将对象的指针存入容器,但带来的问题就是,这些对象的生命周期要自己管理,因为指针类型的元素是没有析构函数的。
有了“右值引用”,上述“经验之谈”可以松动一些了,只要我们使用形如makeVec()这样的函数”造出“我们想要的对象,然后push_back到容器中去,也可以完全避免不必要的复制操作,前提是这个对象的类实现了右值引用参数版本的复制函数(就是那个资源窃取,只复制指针,不复制内存区域的函数)。而新标准下的标准库显然已经为我们实现了vector的右值引用版本的复制函数:-)。
题外话,makeVec()这样的函数本身是没有效率问题的,因为返回值优化(http://efnetcpp.org/wiki/Return_value_optimization)的存在。
4. 不是右值表达式表达的对象就窃取不了了么?
当然可以,伴随右值引用而来的有这么一个“邪恶”的东西,std::move(),它返回传入对象的右值表达,这样我们就可以把想窃取的对象用std::move()包装一下再传给右值引用版本的函数。为什么说他“邪恶”呢,是因为我们需要自己对被窃取的那个对象负责了,因为它被窃取之后是一无所有的,调用它的任何方法也许都会是一场灾难。
5. 关于右值引用还有什么?
还有一个对于泛型编程很有用的“完美转发(perfect forwarding)”,跟“移动语义”一点关系都没有,是恰好在“右值引用”作为一个新语法内容出现后,可以不需要变更老语法的既定表现,而是使用新语法来实现新的语言特性,从而更好的兼容历史代码。关于完美转发本文就不详细说了,感兴趣的读者可以搜索一下,文章还是比较多的哈~
6. 总结
有了右值引用和使用右值引用实现了移动语义的函数,我们可以用可读性更高、封装性更好的代码实现高效率的操作了;
有了右值引用和使用右值引用实现了完美转发的模板库,泛型编码的效率也更高;
前者是运行效率、后者是编码效率的免费午餐~