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性能更差。