C++ vector的emplace_back函數


C++ STL的vector相信大家一定都知道,它是一個一般用來當做可變長度列表的類。在C++11之前,一般給vector插入新元素用得都是push_back函數,比如下面這樣:

std::vector<std::string> list;
list.push_back(std::string("6666"));

這種寫法事實上有很多的冗余計算,我們來分析下,調用這句push_back一共做了哪些操作:
1.執行了std::string的構造函數,傳入"6666"構造出一個std::string,這是一個臨時變量,我們稱它為temp;
2.執行了std::string的拷貝構造函數,將temp的內容拷貝到list的空間中;
3.執行了std::string的析構函數,析構臨時變量temp。
從中可以看到,我們需要執行std::string的兩個構造函數和一個析構函數,還是比較耗費時間的。仔細分析下可以發現,構造函數還是必須要有一個的(因為必須要為list創建一個std::string對象),但我們完全可以省略掉創建和銷毀臨時對象temp的操作,只要我們能保證將"6666"直接傳入為list構造的std::string對象中。
為了實現這個目標,C++11引入了emplace_back函數,它通過完美轉發實現了在vector中插入時直接在容器內構造對象,省略了創建臨時對象的操作。我們看下它的代碼就不難理解:

template<class _Ty,
	class _Alloc = allocator<_Ty>>
	class vector
		: public _Vector_alloc<_Vec_base_types<_Ty, _Alloc>>
{
...
public:
  template<class... _Valty>
  decltype(auto) emplace_back(_Valty&&... _Val)
  {	// insert by perfectly forwarding into element at end, provide strong guarantee
        if (_Has_unused_capacity())
	{
	   return (_Emplace_back_with_unused_capacity(_STD forward<_Valty>(_Val)...));
	}

	_Ty& _Result = *_Emplace_reallocate(this->_Mylast(), _STD forward<_Valty>(_Val)...);

	return (_Result);
  }
...
};

對於上面案例中的list(vectorstd::string)來說,_Ty是std::string,調用list.emplace_back("6666"),則_Valty就是const char*,通過完美轉發機制(forward<_Valty>)最終將傳入的參數_Val(本例中就是"6666")傳入std::string的構造函數中,實現了直接從list中一步到位構造對象,省略了創建臨時對象的過程,從而減少了創建的時間。

下面是我做的實驗,首先定義一個類A,它可以接受一個const char*來構造:

class A
{
public:
	A(const char* c)
	{
		int count = strlen(c);
		content = new char[count];
		memcpy(content, c, count);
	}

	~A()
	{
		if (content)
		{
			delete[] content;
		}
	}

	A(const A& a)
	{
		int count = strlen(a.content);
		content = new char[count];
		memcpy(content, a.content, count);
	}

	A(A && a)
	{
		content = a.content;
		a.content = nullptr;
	}
private:
	char * content = nullptr;;
};

通過以下代碼來測試運行時間:

int main()
{
	std::vector<A> str_list;
	std::vector<A> str_list2;
	std::vector<A> str_list3;
	std::vector<A> str_list4;

	int count = 100000;
        //提前留好空間,防止創建內存干擾
	str_list.reserve(count * 2);
	str_list2.reserve(count * 2);
	str_list3.reserve(count * 2);
	str_list4.reserve(count * 2);

	clock_t start = clock();
	for (int i = 0; i < count; i++)
	{
		A a = "hahahaah";
		str_list.push_back(a);
	}
	clock_t end = clock();
	std::cout << end - start << std::endl;

	start = clock();
	for (int i = 0; i < count; i++)
	{
		str_list2.push_back("hahahaah");
	}
	end = clock();
	std::cout << end - start << std::endl;

	start = clock();
	for (int i = 0; i < count; i++)
	{
		str_list3.emplace_back("hahahaah");
	}
	end = clock();
	std::cout << end - start << std::endl;

	start = clock();
	for (int i = 0; i < count; i++)
	{
		A a = "hahahaah";
		str_list4.emplace_back(a);
	}
	end = clock();
	std::cout << end - start << std::endl;

	system("pause");
	return 0;
}

以上四種情況輸出的運行時間如下(VS2017、Debug、Win32模式下編譯):

125
84
78
106

可以看到,最耗時的就是第一種:

for (int i = 0; i < count; i++)
{
	A a = "hahahaah";
	str_list.push_back(a);
}

因為它就是本文前面說的“創建臨時對象->創建list中的對象->銷毀臨時對象”過程。

str_list2的做法相對好一些,因為雖然也會生成臨時對象,但因為傳入的是右值引用,因此調用的是移動構造函數,相對來說節省一些。
str_list3就是本文上面提到的做法,直接在vector上構造對象,自然是時間最短的。
str_list4還是構造了對象,因此雖然調用的是emplace_back,但傳入的是左值引用A&,所以仍然比較耗時間。不過這也說明,emplace_back也可以像push_back一樣傳入要插入的對象(而不是構造對象的參數),這樣的話走的還是類似push_back的流程,通過調用復制構造函數創建新對象。這種用法從試驗結果上看,起碼不會比push_back性能更差。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM