當我把 push_back 換成 emplace_back 后,代碼性能竟然......
大家好,我是小康。
你有沒有過這樣的經歷?寫了一大堆代碼,明明邏輯沒問題,程序跑得卻像蝸牛一樣慢。特別是當你在處理大量數據,往容器里瘋狂塞東西的時候。
如果你經常和 C++ 的 vector、list 這些容器打交道,那么今天這篇文章絕對值得你花幾分鐘時間——因為我要告訴你一個小技巧,它能讓你的代碼不僅寫起來更爽,還能跑得更快!
它就是容器的"隱藏技能":emplace_back()。
"又是一個新函數?我的push_back不香了嗎?"
別急,咱們先來看個例子,感受一下這兩者的區別:
#include <vector>
#include <string>
class Person {
public:
Person(std::string name, int age) : m_name(name), m_age(age) {
printf("構造了一個人:%s, %d歲\n", name.c_str(), age);
}
// 拷貝構造函數
Person(const Person& other) : m_name(other.m_name), m_age(other.m_age) {
printf("拷貝構造了一個人:%s\n", m_name.c_str());
}
private:
std::string m_name;
int m_age;
};
int main() {
std::vector<Person> people;
printf("=== 使用push_back ===\n");
people.push_back(Person("張三", 25));
people.clear(); // 清空容器
printf("\n=== 使用emplace_back ===\n");
people.emplace_back("李四", 30);
}
運行這段代碼,你會看到這樣的輸出:
=== 使用push_back ===
構造了一個人:張三, 25歲
拷貝構造了一個人:張三
=== 使用emplace_back ===
構造了一個人:李四, 30歲
看出區別了嗎?
- 使用push_back時,我們先創建了一個臨時的 Person 對象,然后 vector 把它拷貝到容器里
- 而使用emplace_back時,我們直接傳入構造 Person 所需的參數,vector 直接在容器內部構造對象
結果就是:push_back額外調用了一次拷貝構造函數,而emplace_back沒有!
"所以emplace_back就是直接傳構造函數參數?"
沒錯!這就是它最大的特點。
- push_back(x) 需要你先構造好一個對象x,然后把它放進容器
- emplace_back(args...) 則是直接把構造函數的參數 args 傳進去,在容器內部構造對象
這個差別看似小,實際上在性能上卻能帶來很大的提升,尤其是當:
對象構造成本高(比如有很多成員變量)
拷貝成本高(比如內部有動態分配的內存)
你需要插入大量對象時
"來點實際的例子!"
好的,我們來看一個真正能展示差異的例子。我們創建一個拷貝成本真正很高的類,這樣才能看出 emplace_back 的威力:
#include <vector>
#include <string>
#include <chrono>
#include <iostream>
#include <memory>
// 設計一個拷貝成本很高的類
class ExpensiveToCopy {
public:
// 構造函數 - 創建一個大數組
ExpensiveToCopy(const std::string& name, int dataSize)
: m_name(name), m_dataSize(dataSize) {
// 分配大量內存,模擬昂貴的資源
m_data = new int[dataSize];
for (int i = 0; i < dataSize; i++) {
m_data[i] = i; // 初始化數據
}
}
// 拷貝構造函數 - 非常昂貴,需要復制整個大數組
ExpensiveToCopy(const ExpensiveToCopy& other)
: m_name(other.m_name), m_dataSize(other.m_dataSize) {
// 深拷貝,非常耗時
m_data = new int[m_dataSize];
for (int i = 0; i < m_dataSize; i++) {
m_data[i] = other.m_data[i];
}
// 輸出提示以便觀察拷貝構造函數的調用情況
std::cout << "拷貝構造: " << m_name << std::endl;
}
// 析構函數
~ExpensiveToCopy() {
delete[] m_data;
}
// 禁用賦值運算符以簡化例子
ExpensiveToCopy& operator=(const ExpensiveToCopy&) = delete;
private:
std::string m_name;
int* m_data;
int m_dataSize;
};
// 計時輔助函數
template<typename Func>
long long timeIt(Func func) {
auto start = std::chrono::high_resolution_clock::now();
func();
auto end = std::chrono::high_resolution_clock::now();
return std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();
}
int main() {
const int COUNT = 100; // 對象數量,減少一點以便看到輸出
const int DATA_SIZE = 100000; // 每個對象中數組的大小
std::cout << "=== 測試push_back ===\n";
long long pushTime = timeIt([&]() {
std::vector<ExpensiveToCopy> objects;
objects.reserve(COUNT); // 預分配空間避免重新分配的影響
for (int i = 0; i < COUNT; i++) {
// 創建臨時對象然后放入vector
// 這個過程會調用拷貝構造函數
objects.push_back(ExpensiveToCopy("對象" + std::to_string(i), DATA_SIZE));
}
});
std::cout << "\n=== 測試emplace_back ===\n";
long long emplaceTime = timeIt([&]() {
std::vector<ExpensiveToCopy> objects;
objects.reserve(COUNT); // 預分配空間避免重新分配的影響
for (int i = 0; i < COUNT; i++) {
// 直接傳遞構造函數參數
// 直接在vector內部構造對象,避免了拷貝
objects.emplace_back("對象" + std::to_string(i), DATA_SIZE);
}
});
std::cout << "\npush_back耗時: " << pushTime << " 微秒" << std::endl;
std::cout << "emplace_back耗時: " << emplaceTime << " 微秒" << std::endl;
double percentDiff = (static_cast<double>(pushTime) / emplaceTime - 1.0) * 100.0;
std::cout << "性能差異: push_back比emplace_back慢了 " << percentDiff << "%" << std::endl;
}
在我的電腦上,大約是這樣的結果:
=== 測試emplace_back ===
push_back耗時: 66979 微秒
emplace_back耗時: 35858 微秒
性能差異: push_back比emplace_back慢了 86.7896%
這意味著push_back比emplace_back慢了約86%!這可不是小數目,尤其是在處理大對象時。
"看起來emplace_back完勝啊!為什么還有人用push_back?"
好問題!emplace_back雖然在大多數情況下更快,但并不是所有場景都適合用它:
- 當你已經有一個現成的對象時,push_back可能更直觀
- 對于基本類型(int, double等),兩者性能差異可以忽略不計
- 對于某些編譯器優化情況,比如移動語義,差距可能不明顯
來看一個例子,說明什么時候兩者其實差不多:
std::vector<int> numbers;
// 對于基本類型,這兩個是等價的
numbers.push_back(42);
numbers.emplace_back(42);
// 如果已經有一個現成的對象
std::string name = "張三";
std::vector<std::string> names;
// 這種情況下,如果 string 支持移動構造,兩者性能接近
names.push_back(name); // 拷貝name
names.push_back(std::move(name)); // 移動name(推薦)
names.emplace_back(name); // 拷貝name
names.emplace_back(std::move(name)); // 移動name(推薦)
"完美轉發是什么鬼?聽說emplace_back跟這個有關?"
沒錯!emplace_back的強大之處,部分來自于它使用了"完美轉發"(Perfect Forwarding)技術。
簡單來說,完美轉發就是把函數參數"原汁原味"地傳遞給另一個函數,保持它的所有特性(比如是左值還是右值,是const還是non-const)。
在C++中,這通常通過模板和std::forward實現:
template <typename... Args>
void emplace_back(Args&&... args) {
// 在容器內部直接構造對象
// 完美轉發所有參數
new (memory_location) T(std::forward<Args>(args)...);
}
這樣的設計讓emplace_back能夠接受任意數量、任意類型的參數,并且完美地轉發給對象的構造函數。這就是為什么你可以直接這樣寫:
people.emplace_back("張三", 25); // 直接傳構造函數參數
而不需要先構造一個對象。
"還有其他 emplace 系列函數嗎?"
是的!STL容器中有一系列的emplace函數:
- vector、deque、list: emplace_back()
- list, forward_list: emplace_front()
- 所有容器: emplace()(在指定位置構造元素)
- 關聯容器(map, set等): emplace_hint()(帶提示的插入)
它們的共同點是:直接在容器內部構造元素,而不是先構造再拷貝/移動。
實戰建議:什么時候用 emplace_back?
- 復雜對象插入:當你要插入的對象構造成本高、拷貝代價大時
- 大量數據操作:需要插入大量元素時,性能差異會更明顯
- 直接傳參更方便時:比如插入 pair 到 map
// 不那么優雅
std::map<int, std::string> m;
m.insert(std::make_pair(1, "one"));
// 更優雅,也更高效
m.emplace(1, "one");
- 臨時對象場景:當你需要創建臨時對象并插入容器時
總結
emplace_back本質上是通過減少不必要的對象創建和拷貝來提升性能。它利用了 C++ 的完美轉發功能,讓你可以直接傳遞構造函數參數,而不需要先創建臨時對象。
在處理復雜對象或大量數據時,這種優化尤為明顯。當然,對于簡單類型或已有對象,兩者差異不大。
所以下次當你在寫:
myVector.push_back(MyClass(arg1, arg2));
的時候,不妨試試:
myVector.emplace_back(arg1, arg2);
代碼更簡潔,運行更高效,何樂而不為呢?
記住,在編程世界里,這種看似微小的優化,累積起來就是質的飛躍!