C++11:改善程序性能

目录

  • 1 右值引用
    • 1.1 右值引用的特性
    • 1.2 右值引用避免深拷贝
  • 2 move语义
  • 3 forward和完美转发
  • 4 emplace_back 减少内存拷贝和移动
  • 5 unordered container无序容器

1 右值引用

1.1 右值引用的特性

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

int i = 0;

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

void Printvalue (int& i)
{
	std::cout<<"lvalue : "<<i<<std::endl;
}

void Printvalue (int& & i)
{
	std::cout<<"rvalue : "<<i<<std::endl;
}

void Forward (int& & i)
{
	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" <<endl;
	}
	
	~A()
	{
		cout << "destruct" <<endl;
		delete m_ptr;
	}
	
private:
	int* m_ptr;
};

/为了避免返回值优化,此函数故意这样写
A Get (bool flag)
{
	A a;
	A b;
	if (flag)
		return a;
	else
		return 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" <<endl;
	}
	
	A(A&& a) :m_ptr (a.m_ptr)
	{
		a.m ptr = nullptr;
		cout << "move construct: "<<endl;
	}

	~A()
	{
		cout << "destruct" <<endl;
		delete m_ptr;
	}
	
private:
	int* m_ptr;
};

int main()
{
	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<std::string> tokens;	//省略初始化……
	std::list<std::string> t = tokens;
}
std::list<std::string> tokens;
std::list<std::string> t = std::move(tokens);

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

3 forward和完美转发

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

template <typename T>
void forwardvalue(T& val)
{
	processvalue(val);	//右值参数会变成左值
}

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

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

template <typename T>
void PrintT(int& t)
{
	std::cout << "lvalue" << std::endl;
}

template <typename T>
void PrintT(T&& t)
{
	std::cout << "rvalue" << std::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));
}

输出结果:
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<int, Complicated> 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<Complicated> 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<std::string>()(k.first) ^
			(std::hash<std::string>()(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<std::string, std::string> m1;
	std::unordered_map<int, std::string> m2 = { { 1, "foo" }, { 2, "bar" }, { 3, "baz" } };
	std::unordered_map<int, std::string> m3 = m2;
	std::unordered_map<int, std::string> m4 = std::move(m2);

	std::vector<std::pair<std::bitset<8>, int>> v = { { 0x12, 1 }, { 0x01, -1 } };
	std::unordered_map<std::bitset<8>, double> m5(v.begin(), v.end());

	std::unordered_map<Key, std::string, KeyHash, KeyEqual> m6 =
	{ { { "John", "Doe" }, "example" }, { { "Mary", "Sue" }, "another" } };

	system("pause");
	return 0;
}

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

你可能感兴趣的:(C++,c++,开发语言)