文章目錄
  1. 1. 右值引用和性能优化
    1. 1.1. &&的特性
    2. 1.2. 右值引用优化性能,避免深拷贝
  2. 2. move语义
  3. 3. forward和完美转发
  4. 4. emplace_back
  5. 5. unordered container 无序容器


作者:Frank
时间:2016-12-06

该部分是《深入应用C++11代码优化和工程级应用》的第二章,在这章中主要介绍了右值引用及其对性能的优化,move语义、forward和完美转发、emplace_back以及无序容器,以下就对其中涉及的主要知识点进行总结。

右值引用和性能优化

C++11中新增加右值引用类型,表示为T&&。左值是指那些表达式结束后依然存在的持久对象,而右值是指在表达式结束后就不再存在的临时对象。一个简单的区分左值和右值的方法是:看能不能对对象取地址,如果能,则为左值,如果不能,则为右值。在C++11中,右值由两个概念构成,一个是将亡值(xvalue,expiring value),体格是纯右值(prvalue,pureRvalue)。C++11中所有的值必属于左值,将亡值、纯右值之一。
示例:

1
int i=0;

其中,i是左值,0是字面量,就是右值。在上面的代码中,i可以被引用,0就不可以了。

&&的特性

无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不具有所绑定对象的内存,只是该对象的一个别名。

1
2
3
4
5
6
7
template<typename T>
void f(T&& param){
}

f(10);//10是右值
int x=10;
f(x);//x是左值

在上述例子中,param有时是左值,有时是右值,这表示param是一个未定的引用类型,这个未定的引用类型称为universal references。它必须被初始化,它是左值还是右值取决于它的初始化,如果&&被一个左值初始化,它就是一个左值,如果它被一个右值初始化,它就是一个右值。需要注意的是,只有当&&发生自动类型推断时,&&才是一个universal references。而且universal references仅在T&&下发生,任何一点附加条件都会使之失效,而变成一个普通的右值引用。
由于存在T&&这种未定的引用类型,当它作为参数时,有可能被一个左值引用或者被一个右值引用的参数初始化,这是经过类型推导的T&&类型,相比右值引用(&&)会发生类型的变化,这种变化被称为引用折叠。C++11中的引用折叠规则如下:

  1. 所有的右值引用叠加到右值引用上仍然是一个右值引用。
  2. 所有的其他引用类型之间的叠加都将变成一个左值引用。

&&的总结如下:

  1. 左值和右值是独立于它们的类型的,右值引用类型可能是左值也可能是右值;
  2. auto&&或函数参数类型自动推导的T&&是一个未定的引用类型,称为universal references,它可能是左值引用也可能是右值引用,取决于初始化的值类型;
  3. 所有的右值引用叠加到右值引用上仍然是一个右值引用,而其他引用叠加都变成左值引用。当T&&为模板参数时,输入左值,它会变成左值引用,输入右值则变为具名的右值引用;
  4. 编译器会将已命名的右值引用视为左值,而将未命名的右值引用视为右值。

右值引用优化性能,避免深拷贝

在传统的C++构造函数中,无论是拷贝构造函数还是赋值构造函数,在使用过程中都会产生不必要的拷贝,因此可以利用移动构造(move construct)来代替。移动构造的参数时一个右值引用类型的参数,这里没有深拷贝,只有浅拷贝,这样就避免了对临时对象的深拷贝,提高了性能。移动语义可以将资源通过浅拷贝的方式从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,可以大幅度提高C++应用程序的性能,消除临时对象的维护对性能的影响。代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class MyString{
char* m_data;
size_t m_len;
void copy_data(const char* s){
m_data=new char[m_len+1];
memcpy(_data,s,m_len);
m_data[m_len]='\0';
}
public:
MyString(){
m_data=NULL;
m_len=0;
}

MyString(const char* p){
m_len=strlen(p);
copy_data(p);
}

MyString(const MyString* str){
m_len=str.m_len;
copy_data(str.m_data);
std::cout<<"copy constructor is called! souce:"<<str.m_data<<std::endl;
}

MyString(MyString&& str){
std::cout<<"move constructor is called!source:"<<str.m_data<<std::endl;
m_len=std.m_len;
m_data=std.m_data;
str.m_len=0;
str.m_data=0;
}

MyString& operator=(MyString&& str){
std::cout<<"move assignment is called! source:"<<str.m_data<<std::endl;
if(this!=&str){
m_len=str.m_len;
m_data=str.m_data;
str.m_len=0;
str.m_data=0;
}
}

virtual ~MyString(){
if(m_data)free(m_data);
}
};

void test(){
MyString a;
a=MyString("Hello");
std::vector<MyString> vec;
vec.push_back(MyString("World"));
}

有了右值引用和移动语义,在设计和实现类时,对于需要动态申请大量资源的类,应该设计右值引用的拷贝构造函数和赋值函数,以提高应用程序的效率。需要注意的是,一般在提供右值引用的构造函数的同时,也会提供常量左值引用的拷贝构造函数,以保证移动不成还可以使用拷贝构造。

move语义

移动语义是通过右值引用来匹配临时值的。为了让普通的左值也能借助移动语义来优化性能,C++11提供了std::move方法来将左值转换为右值,从而方便移动语义。move是将对象的状态或者所有权从一个对象转移到另一个对象,只有转移,没有内存拷贝。使用move语义几乎没有任何代价,只是转换了资源的所有权。实际上是将左值引用变成右值引用,然后引用move语义调用构造函数,就避免了拷贝,提高了程序性能。当一个对象内部有较大的堆内存或者动态数组时,很有必要写move语义的拷贝构造函数和赋值构造函数。避免无谓的深拷贝,以提高性能。
move只是转移了资源的控制权,本质上是将左值强制转换为右值引用,以用于move语义,避免含有资源的对象发生无谓的拷贝。move对于拥有形如对内存、文件句柄等资源的成员对象有效。如果说是一些基本类型,比如int,使用move仍会发生拷贝,所以说move对含有资源的对象来说更加有意义。

forward和完美转发

C++中提供了一个函数std::forward函数来实现语义的完美转发,不管参数是T&&这种未定义的引用还是明确的左值引用或者右值引用,它会按照参数本来的类型转发。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
template<typename T>
void printT(T& t){
scout<<"lvalue"<<endl;
}

template<typename T>
void printT(T&& t){
cout<<"rvalue"<<endl;
}

template<typename T>
void TestForward(T&& v){
printT(v);
printT(std::forward<T>(v));
printT(std::move(v));
}

void Test(){
TestForward(1);
int x=1;
TestForward(x);
TestForward(std::forward<int>(x));
}

利用右值引用、完美转发再结合可变函数,可以写一个万能的函数包装器,如下:

1
2
3
4
template<class Function,class... Args>
inline auto FuncWrapper(Function&& f,Args&& ... args)->decltype(f(std::forward<Args>(args)...)){
return f(std::forward<Args>(args)...);
}

emplace_back

emplace_back能就地通过参数构造对象,不需要拷贝或者移动内存,相比push_back能更好的避免内存的拷贝和移动,使容器插入元素的性能得到进一步的提升,在大多数情况下应该优先使用emplace_back来代替push_back。
emplace_back的用法比较简单,直接通过构造函数的参数就可以构造对象,因此,也要求对象有相应的构造函数,如果没有对应的构造函数,编译器会报错。需要注意的是,还不能完全用emplace_back来代替push_back等老接口,因为在某些场景下并不能直接使用emplace来进行就地构造。

unordered container 无序容器

C++11增加了无序容器unordered_map/unordered_multimap和unordered_set/unordered_multiset,由于这些容器中的元素是不排序的,因此比有序容器map/multimap和set/multiset效率更高。map和set内部是红黑树,在插入元素时会自动排序,而无序容器内部是散列表,通过哈希,而不是排序来快速操作元素,使得效率高。由于无序容器内部是散列表,因此无序容器的key需要提供hash_value函数,其他用法和map/set的用法是一样的。



转载请注明出处

文章目錄
  1. 1. 右值引用和性能优化
    1. 1.1. &&的特性
    2. 1.2. 右值引用优化性能,避免深拷贝
  2. 2. move语义
  3. 3. forward和完美转发
  4. 4. emplace_back
  5. 5. unordered container 无序容器