你想知道的 std::vector::push_back 和 std::vector::emplace_back

引言

C++ 11 後,標準庫容器 std::vector 包含了成員函數 emplaceemplace_backemplace 在容器指定位置插入元素,emplace_back 在容器末尾添加元素。

emplaceemplace_back 原理類似,本文僅討論 push_backemplace_back

定義

首先看下 Microsoft Docs 對 push_backemplace_back 的定義:

  • push_back:Adds an element to the end of the vector.
  • emplace_back:Adds an element constructed in place to the end of the vector.

兩者的定義我用了加粗字體作區分,那麼現在問題就在於什麼叫做 constructed in place

再來看下官方文檔(www.cplusplus.com)怎麼介紹 emplace_back 的:

template <class… Args>
void emplace_back (Args&&… args);

Inserts a new element at the end of the vector, right after its current last element. This new element is constructed in place using args as the arguments for its constructor.
This effectively increases the container size by one, which causes an automatic reallocation of the allocated storage space if -and only if- the new vector size surpasses the current vector capacity.
The element is constructed in-place by calling allocator_traits::construct with args forwarded.
A similar member function exists, push_back, which either copies or moves an existing object into the container.

簡而言之,push_back 會構造一個臨時對象,這個臨時對象會被拷貝或者移入到容器中,然而 emplace_back 會直接根據傳入的參數在容器的適當位置進行構造而避免拷貝或者移動。

為什麼我們有了 emplace_back 還需要 push_back

這部分內容進一步對如何區分 push_backemplace_back 做了解答。
Stack Overflow 有一項回答我認為已經解釋的較為清楚,因此這裡部分轉譯過來。

翻譯帶有個人理解,非直譯,原文參考://stackoverflow.com/questions/10890653/why-would-i-ever-use-push-back-instead-of-emplace-back

以下為譯文:

關於這個問題我在過去 4 年思考良多,我敢說大多數關於 push_backemplace_back 的解釋都不夠完善。

去年,我在一次關於 C++ 的介紹中(鏈接參考原文)討論了 push_backemplace_back 的相關議題,這兩者最主要的區別來自於:是使用隱式構造函數還是顯示構造函數(implicit vs. explicit constructors)。

先看下面的示例:

std::vector<T> v;

v.push_back(x);
v.emplace_back(x);

傳統觀點認為 push_back 會構造一個臨時對象,這個臨時對象會被移入到 v 中,然而 emplace_back 會直接根據傳入的參數在適當位置進行構造而避免拷貝或者移動。從標準庫程式碼的實現角度來說這是對的,但是對於提供了優化的編譯器來講,上面示例中最後兩行表達式生成的程式碼其實沒有區別。

真正的區別在於,emplace_back 更加強大,它可以調用任何類型的(只要存在)構造函數。而 push_back 會更加嚴謹,它只調用隱式構造函數。隱式構造函數被認為是安全的。如果能夠通過對象 T 隱式構造對象 U,就認為 U 能夠完整包含 T 的所有內容,這樣將 T 傳遞給 U 通常是安全的。正確使用隱式構造的例子是用 std::uint32_t 對象構造 std::uint64_t 對象,錯誤使用隱式構造的例子是用 double 構造 std::uint8_t

我們必須在編碼時小心翼翼。我們不想使用強大/高級的功能,因為它越是強大,就越有可能發生意想不到的錯誤。如果想要調用顯示構造函數,那麼就調用 emplace_back。如果只希望調用隱式構造函數,那麼請使用更加安全的 push_back

再看個示例:

std::vector<std::unique_ptr<T>> v;
T a;
v.emplace_back(std::addressof(a)); // compiles
v.push_back(std::addressof(a)); // fails to compile

std::unique_ptr<T> 包含了顯示構造函數通過 T* 進行構造。因為 emplace_back 能夠調用顯示構造函數,所以傳遞一個裸指針並不會產生編譯錯誤。然而,當 v 超出了作用域,std::unique_ptr<T> 的析構函數會嘗試 delete 類型 T* 的指針,而類型 T* 的指針並不是通過 new 來分配的,因為它保存的是棧對象的地址,因此 delete 行為是未定義的。

這不是為了示例而特意寫的程式碼,而是一個我遇到的實際問題。原本 vstd::vector<T *> 類型,遷移到 C++ 11 後,我修改為 std::vector<std::unique_ptr<T>>。並且,那時我錯誤地認為 emplace_back 能夠做 push_back 所能做的所有事情,因此將 push_back 也改為了 emplace_back

如果我保留使用更加安全的 push_back,那麼我會立馬發現這個 bug。不幸的是,我意外地隱藏了這個 bug 並直到幾個月後才重新發現它。

引用