C++11:改善程序性能
创始人
2025-06-01 22:26:35
0

1 右值引用

1.1 右值引用的特性

C++11中所有的值必属于左值、将亡值、纯右值三者之一。将亡值和纯右值都属于右值。区分表达式的左右值属性有一个简便方法:若可对表达式用&符取址,则为左值,否则为右值。
比如,简单的赋值语句:

int i = 0;

在这条语句中,i是左值,0是字面量,就是右值。在上面的代码中,i可以被引用,0就不可以了。字面量都是右值。
右值引用就是对一个右值进行引用的类型。因为右值不具名,所以我们只能通过引用的方式找到它。
无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所绑定对象的内存,只是该对象的一个别名。通过右值引用的声明,该右值又“重获新生”,其生命周期与右值引用类型变量的生命周期一样,只要该变量还活着,该右值临时量将会一直存活下去。

void Printvalue (int& i)
{std::cout<<"lvalue : "<std::cout<<"rvalue : "<Printvalue (i);
}int main ()
{int i = 0;Printvalue(i);Printvalue(1);Forward (2);
}

将输出如下结果:
lvalue : 0
rvalue : 1
lvaue : 2
Forward函数接收的是一个右值,但在转发给PrintValue时又变成了左值,因为在Forward中调用PrintValue时,右值i变成了一个命名的对象,编译器会将其当作左值处理。

1.2 右值引用避免深拷贝

class A
{
public:A() :m_ptr (new int (0)){cout <<"construct"<< endl ;}A (const A& a) :m_ptr (new int(*a.m_ptr))	//深拷贝{cout << "copy construct" <cout << "destruct" <A a;A b;if (flag)return a;elsereturn b;
}int main()
{A a = Get(false);	//运行正确
}

上面的代码将输出:
construct
construct
copy construct
destruct
destruct
destruct
这样就可以保证拷贝构造时的安全性,但有时这种拷贝构造却是不必要的,比如上面代码中的拷贝构造就是不必要的。上面代码中的Get 函数会返回临时变量,然后通过这个临时变量拷贝构造了一个新的对象b,临时变量在拷贝构造完成之后就销毁了,如果堆内存很大,那么,这个拷贝构造的代价会很大,带来了额外的性能损耗。有没有办法避免临时对象的拷贝构造呢?答案是肯定的。看下面的代码:

class A
{
public:A() :m_ptr (new int (0)){cout <<"construct"<< endl ;}A (const A& a) :m_ptr (new int(*a.m_ptr))	//深拷贝{cout << "copy construct" <a.m ptr = nullptr;cout << "move construct: "<cout << "destruct" <A a = Get(false);	//运行正确
}

上面的代码将输出:
construct
construct
move construct
destruct
destruct
destruct
上面的代码中没有了拷贝构造,取而代之的是移动构造(Move Construct)。从移动构造函数的实现中可以看到,它的参数是一个右值引用类型的参数A&&,这里没有深拷贝,只有浅拷贝,这样就避免了对临时对象的深拷贝,提高了性能。这里的A&&用来根据参数是左值还是右值来建立分支,如果是临时值,则会选择移动构造函数。移动构造函数只是将临时对象的资源做了浅拷贝,不需要对其进行深拷贝,从而避免了额外的拷贝,提高性能。这也就是所谓的移动语义(move语义),右值引用的一个重要目的是用来支持移动语义的。
移动语义可以将资源(堆、系统对象等)通过浅拷贝方式从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,可以大幅度提高C++应用程序的性能,消除临时对象的维护(创建和销毁)对性能的影响。

2 move语义

move实际上并不能移动任何东西,它唯一的功能是将一个左值强制转换为一个右值引用°,使我们可以通过右值引用使用该值,以用于移动语义。强制转换为右值的目的是为了方便实现移动构造。
这种move语义是很有用的,比如一个对象中有一些指
针资源或者动态数组,在对象的赋值或者拷贝时就不需要拷贝这些资源了。在C++11之前拷贝构造函数和赋值函数可能要像下面这样定义。假设一个A对象内部有一个资源m _ptr:

A& A::operator=(const A& rhs)
{//销毁m_ptr指向的资源//复制rhs.m_ptr所指的资源,并使m_ptr指向它
}

同样A的拷贝构造函数也是这样。假设这样来使用A:

A foo(); 	//foo是一个返回值为×的函数
A a;
a = foo() ;

最后一行将会发生如下操作:销毁a所持有的资源;复制foo返回的临时对象所拥有的资源;销毁临时对象,释放其资源。
上面的过程是可行的,但是更有效率的办法是直接交换a和临时对象中的资源指针,然后让临时对象的析构函数去销毁a原来拥有的资源。换句话说,当赋值操作符的右边是右值的时候,我们希望赋值操作符被定义成下面这样:

A& A::operator=(const A&& rhs)
{//转移资源的控制权,无须复制
}

仅仅转移资源的所有者,将资源的拥有者改为被赋值者,这就是所谓的move语义。再看一个例子,假设一个临时容器很大,赋值给另一个容器。

{std::list tokens;	//省略初始化……std::list t = tokens;
}
std::list tokens;
std::list t = std::move(tokens);

如果不用std:move,拷贝的代价很大,性能较低。使用move几乎没有任何代价,只是转换了资源的所有权。实际上是将左值变成右值引用,然后应用move语义调用构造函数,就避免了拷贝,提高了程序性能。当一个对象内部有较大的堆内存或者动态数组时,很有必要写move语义的拷贝构造函数和赋值函数,避免无谓的深拷贝,以提高性能。事实上,C++中所有的容器都实现了move语义,方便我们实现性能优化。
这里也要注意对move语义的误解,move只是转移了资源的控制权,本质上是将左值强制转换为右值引用,以用于move语义,避免含有资源的对象发生无谓的拷贝。move对于拥有形如对内存、文件句柄等资源的成员的对象有效。如果是一些基本类型,比如 int 和char[10]数组等,如果使用move,仍然会发生拷贝(因为没有对应的移动构造函数),所以说move对于含资源的对象来说更有意义。

3 forward和完美转发

上节中介绍的右值引用类型是独立于值的,一个右值引用参数作为函数的形参,在函数内部再转发该参数的时候它已经变成一个左值了,并不是它原来的类型了。比如:

template 
void forwardvalue(T& val)
{processvalue(val);	//右值参数会变成左值
}template 
void forwardvalue(const T& val)
{processvalue(val);	//参数都变成常量左值引用了
}

都不能按照参数的本来的类型进行转发。
因此,我们需要一种方法能按照参数原来的类型转发到另一个函数,这种转发被称为完美转发。所谓完美转发( Perfect Forwarding),是指在函数模板中,完全依照模板的参数的类型(即保持参数的左值、右值特征),将参数传递给函数模板中调用的另外一个函数。C++11中提供了这样的一个函数std::forward,它是为转发而生的,不管参数是T&&这种未定的引用还是明确的左值引用或者右值引用,它会按照参数本来的类型转发。看这个例子:

template 
void PrintT(int& t)
{std::cout << "lvalue" << std::endl;
}template 
void PrintT(T&& t)
{std::cout << "rvalue" << std::endl;
}template 
void TestForward(T&& v)
{PrintT(v);PrintT(std::forward(v));PrintT(std::move(v));
}void Test()
{TestForward(1);int x = 1;TestForward(x);TestForward(std::forward(x));
}

输出结果:
lvalue
rvalue
rvalue
lvalue
lvalue
rvalue
lvalue
rvalue
rvalue
分析:
TestForward(1):由于1是右值,所以未定的引用类型T&& v被一个右值初始化后变成了一个右值引用,但是在TestForward函数体内部,调用PrintT(v)时,v又变成了一个左值(因为在这里它已经变成了一个具名的变量, 所以它是一个左值),因此第一个PrintT被调用,打印出“lvaue”。调用PrintT(std::forward(v))时,由于std::forward会按参数原来的类型转发,因此,它还是一个右值(这里已经发生了类型推导,所以这里的T&&不是一个未定的引用类型(关于这点可以参考2.1节),会调用void PrintT(T&& t)函数。调用PrintT(std::move(v))是将v变成一个右值(v本身也是右值),因此,它将输出rvalue。
TestForward(x):未定的引用类型T&& v被一个左值初始化后变成了一个左值引用,因此,在调用PrintT(std::forward(v))时它会被转发到 void PrintT(T& t)。

4 emplace_back 减少内存拷贝和移动

emplace_back能就地通过参数构造对象,不需要拷贝或者移动内存,相比 push_back能更好地避免内存的拷贝与移动,使容器插入元素的性能得到进一步提升。在大多数情况下应该优先使用emplace_back来代替push_back。所有的标准库容器( array除外,因为它的度不可改变,不能插人元素)都增加了类似的方法:emplace、emplace_hint、emplace_frontemplace_after和 emplace_back,关于它们的具体用法可以参考cppreference.com。

#include 
#include 
#include 
#include using namespace std;struct Complicated
{int year;double country;string name;Complicated(int a, double b, string c) : year(a), country(b), name(c){cout << "is constructed" << endl;}Complicated(const Complicated& other) : year(other.year),country(other.country), name(other.name){cout << "is moved" << endl;}
};int main(void)
{map m;int anInt = 4;double aDouble = 5.0;string aString = "C++";cout << "--insert--" << endl;m.insert(make_pair(4, Complicated(anInt, aDouble, aString)));cout << "--emplace--" << endl;m.emplace(4, Complicated(anInt, aDouble, aString));cout << "--emplace_back--" << endl;vector v;v.emplace_back(anInt, aDouble, aString);cout << "--push_back--" << endl;v.push_back(Complicated(anInt, aDouble, aString));system("pause");return 0;
}

输出如下:

--insert--
is constucted
is moved
is moved
--emplace--
is constucted
is moved
--emplace_back--
is constucted
--push_back--
is constucted
is moved
is moved

用map的 insert方法插入元素时有两次内存移动,而用emplace时只有一次内存移动;用vector的 push_back插入元素时有两次移动内存,而用emplace_back时没有内存移动,是直接构造的。
可以看到, emplace/emplace_back 的性能比之前的insert和 push_back 的性能要提高很多,我们应该尽量用emplace/emplace_back 来代替原来的插入元素的接口以提高性能。需要注意的是,我们还不能完全用emplace_back 来取代push_back 等老接口,因为在某些场景下并不能直接使用emplace来进行就地构造,比如,当结构体中没有提供相应的构造函数时就不能用emplace了,这时就只能用push_back。

5 unordered container无序容器

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

#include 
#include 
#include 
#include 
#include 
#include struct Key
{std::string first;std::string second;
};struct KeyHash
{std::size_t operator()(const Key& k) const{return std::hash()(k.first) ^(std::hash()(k.second) << 1);}
};struct KeyEqual
{bool operator()(const Key& lhs, const Key& rhs) const{return lhs.first == rhs.first && lhs.second == rhs.second;}
};int main(void)
{std::unordered_map m1;std::unordered_map m2 = { { 1, "foo" }, { 2, "bar" }, { 3, "baz" } };std::unordered_map m3 = m2;std::unordered_map m4 = std::move(m2);std::vector, int>> v = { { 0x12, 1 }, { 0x01, -1 } };std::unordered_map, double> m5(v.begin(), v.end());std::unordered_map m6 ={ { { "John", "Doe" }, "example" }, { { "Mary", "Sue" }, "another" } };system("pause");return 0;
}

对于基本类型来说,不需要提供Hash函数和比较函数,用法上和 map/set一样,对于自定义的结构体,就稍微复杂一些,需要提供函数和比较函数。

相关内容

热门资讯

监控摄像头接入GB28181平... 流程简介将监控摄像头的视频在网站和APP中直播,要解决的几个问题是:1&...
Windows10添加群晖磁盘... 在使用群晖NAS时,我们需要通过本地映射的方式把NAS映射成本地的一块磁盘使用。 通过...
protocol buffer... 目录 目录 什么是protocol buffer 1.protobuf 1.1安装  1.2使用...
educoder数据结构与算法...                                                   ...
MySQL下载和安装(Wind... 前言:刚换了一台电脑,里面所有东西都需要重新配置,习惯了所...
MFC文件操作  MFC提供了一个文件操作的基类CFile,这个类提供了一个没有缓存的二进制格式的磁盘...
在Word、WPS中插入AxM... 引言 我最近需要写一些文章,在排版时发现AxMath插入的公式竟然会导致行间距异常&#...
有效的括号 一、题目 给定一个只包括 '(',')','{','}'...
Fluent中创建监测点 1 概述某些仿真问题,需要创建监测点,用于获取空间定点的数据࿰...
【Ctfer训练计划】——(三... 作者名:Demo不是emo  主页面链接:主页传送门 创作初心ÿ...