C++霧中風景9:emplace_back與可變長模板


C++11的版本在vector容器添加了emplace_back方法,相對於原先的push_back方法能夠在一定程度上提升vector容器的表現性能。所以我們從STL源碼角度來切入,看看這兩種方法有什么樣的區別,新引進的方法又有什么可學習參考之處。

1.emplace_back的用法

emplace_back方法最大的改進就在與可以利用類本身的構造函數直接在內存之中構建對象,而不需要調用類的拷貝構造函數移動構造函數

舉個栗子,假設如下定義了一個時間類time,該類同時定義了拷貝構造函數移動構造函數

class time {
private:
	int hour;
	int minute;
	int second;

public:
	time(int h, int m, int s) :hour(h), minute(m), second(s) {
	}

	time(const time& t) :hour(t.hour), minute(t.minute), second(t.second) {
		cout << "copy" << endl;
	}

	time(const time&& t) noexcept:hour(t.hour),minute(t.minute),second(t.second) {
		cout << "move" << endl;
	}
};

main方法之中執行下面的代碼邏輯:

int main()
{
	vector<time> tlist;
	time t(1, 2, 3);
	tlist.emplace_back(t);
	tlist.emplace_back(2, 3, 4);  //直接調用了time的構造函數在vector的內存之中建立起新的對象

	getchar();
}

執行結果:

  copy                    
  move (這次拷貝構造函數的調用是因為vector本身的擴容,也就是移動之前的已經容納的time對象)

由上述代碼我們看到time對象可以直接利用emplace_back方法在vector上構造對象,通過這樣的方式來減少不必要的內存操作。(省去了拷貝構造的環節)。同樣的在main之中執行下面的代碼邏輯:

int main()
{
	vector<time> tlist;
	time t(1, 2, 3);
	tlist.emplace_back(move(t)); //調用move函數使time對象成為右值,可以利用移動構造函數來拷貝對象
	tlist.emplace_back(2, 3, 4);  //直接調用了time的構造函數在vector的內存之中建立起新的對象

	getchar();
}

執行結果:

  move                   
  move (這次拷貝構造函數的調用是因為vector本身的擴容,也就是移動之前的已經容納的time對象)

通過這樣的方式也減少不必要的內存操作。(省去了移動構造的環節)。所以這就是為什么在C++11之后提倡大家使用emplace_back來代替舊代碼之中的push_back函數。如下面的代碼所示,在push_back底層也是調用了emplace_back來實現對應的操作流程:

void push_back(const _Ty& _Val) {	
       emplace_back(_Val);
}

void push_back(_Ty&& _Val) {	
       emplace_back(_STD move(_Val));
}

2.emplace_back的實現

源碼面前,了無秘密,接下來跟隨筆者直接來看看emplace_back的源代碼,來引出我們今天的主題:

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())
			{
			_Emplace_back_with_unused_capacity(_STD forward<_Valty>(_Val)...);
			}
		else
			{	// reallocate
			const size_type _Oldsize = size();

			if (_Oldsize == max_size())
				{
				_Xlength();
				}

			const size_type _Newsize = _Oldsize + 1;
			const size_type _Newcapacity = _Calculate_growth(_Newsize);
			bool _Emplaced = false;
			const pointer _Newvec = this->_Getal().allocate(_Newcapacity);
			_Alty& _Al = this->_Getal();

			_TRY_BEGIN
			_Alty_traits::construct(_Al, _Unfancy(_Newvec + _Oldsize), _STD forward<_Valty>(_Val)...);
			_Emplaced = true;
			_Umove_if_noexcept(this->_Myfirst(), this->_Mylast(), _Newvec);
			_CATCH_ALL
			if (_Emplaced)
				{
				_Alty_traits::destroy(_Al, _Unfancy(_Newvec + _Oldsize));
				}

			_Al.deallocate(_Newvec, _Newcapacity);
			_RERAISE;
			_CATCH_END

			_Change_array(_Newvec, _Newsize, _Newcapacity);
			}

#if _HAS_CXX17
		return (this->_Mylast()[-1]);
#endif /* _HAS_CXX17 */
		}

通過上述代碼可以看到,emplace_back的流程邏輯很簡單。先檢查vector的容量,不夠的話就擴容,之后便通過_Alty_traits::construct來創建對象。而最終利用強制類似裝換的指針來指向容器類之中對應類的構造函數,並且利用可變長模板將構造函數所需要的內容傳遞過去構造新的對象。

template<class _Objty,
		class... _Types>
		static void construct(_Alloc&, _Objty * const _Ptr, _Types&&... _Args)
		{	// construct _Objty(_Types...) at _Ptr
		::new (const_cast<void *>(static_cast<const volatile void *>(_Ptr)))
			_Objty(_STD forward<_Types>(_Args)...);
		}

emplace_back這里最為巧妙的部分就是利用可變長模板實現了,任意傳參的對象構造。可變長模板是C++11新引進的特性,接下來我們來詳細看看可變長模板是如何來使用,來實現任意長度的參數呢?

3.可變長模板與函數式編程

首先,我們先看看,可變長模板的定義:

    template <class... T>
    void f(T... args);

通過template來聲明參數包args,這個參數包中可以包含0到任意個參數,並且作為函數參數調用。之后我們便可以在函數之中將參數包展開成一個一個獨立的參數。

假設我們有如下需求,需要定義一個max_num函數來求出一組任意參數數字的最大值,在C++11之前的版本或許需要這樣去定義這個函數,也就是說我們需要一個參數來指定對應參數的個數,並且這個過程之中存在參數的類型不一致的潛在風險,並不能在編譯期進行反饋(不能在編譯期進行對於動態語言來說根本不是什么大不了的問題,囧rz):

int max_num(int count, ...)
{
	va_list ap;
	va_start(ap, count);

	int ans = va_arg(ap, int);
	for (int i = 1; i < count; ++i)
	{
		int num = va_arg(ap, int);
		ans = max(ans, num);
	}

	va_end(ap);

	return ans;
}

而利用可變長模板,我們可以很優雅地通過以下的代碼來實現一個這樣的函數:

template<typename t1,typename ...t2> t1 max_num(t1 num, t2 ...args) {
	auto n = max_num(args...);
	return n > num ? n : num;
}
template<typename t1> t1 max_num(t1 num) {
	return num;
}

通過不斷遞歸的方式,提取可變長模板參數之中的首個元素,並且設置遞歸的終止點的方式來依次處理各個元素。這種處理函數的方式本質上就是在通過遞歸的方式處理列表,這種編程思路在函數式編程語言之中十分常見,在C++之中看到這樣的用法,也讓筆者作為C++的入門選手感到很新奇。筆者曾經接觸過Scala與Erlang語言之中大量利用了這種寫法,但是多層遞歸導致的必然是棧調用的開銷變大,利用尾遞歸的方式來優化這樣的寫法,才能減少非必要的函數調用開銷。

4.小結

由emplace_back引申出來不少對C++11新特性的探索,筆者也僅僅做一些拋磚引玉的工作。作為程序員,希望大家能夠堅持不斷動態更新對語言的學習與探索來凝練與高效率的Coding,這也是筆者堅持更新該系列文章的初衷。


免責聲明!

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



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