Tôi đã suy nghĩ về câu hỏi này khá nhiều trong bốn năm qua. Tôi đã đi đến kết luận rằng hầu hết các giải thích về push_back
vs emplace_back
bỏ lỡ bức tranh đầy đủ.
Năm ngoái, tôi đã trình bày tại C ++ Now về Loại trừ trong C ++ 14 . Tôi bắt đầu nói về push_back
so với emplace_back
lúc 13:49, nhưng có một thông tin hữu ích cung cấp một số bằng chứng hỗ trợ trước đó.
Sự khác biệt chính thực sự có liên quan đến các nhà xây dựng ngầm và rõ ràng. Hãy xem xét trường hợp chúng ta có một đối số duy nhất mà chúng ta muốn chuyển đến push_back
hoặc emplace_back
.
std::vector<T> v;
v.push_back(x);
v.emplace_back(x);
Sau khi trình biên dịch tối ưu hóa của bạn có được điều này, không có sự khác biệt giữa hai câu lệnh này về mặt mã được tạo. Sự khôn ngoan truyền thống là push_back
sẽ xây dựng một đối tượng tạm thời, sau đó sẽ được chuyển vào v
trong khi emplace_back
sẽ chuyển tiếp đối số và xây dựng nó trực tiếp tại chỗ mà không có bản sao hoặc di chuyển. Điều này có thể đúng dựa trên mã như được viết trong các thư viện tiêu chuẩn, nhưng nó làm cho giả định sai lầm rằng công việc của trình biên dịch tối ưu hóa là tạo mã bạn đã viết. Công việc tối ưu hóa của trình biên dịch thực sự là tạo mã mà bạn sẽ viết nếu bạn là một chuyên gia về tối ưu hóa cụ thể nền tảng và không quan tâm đến khả năng bảo trì, chỉ là hiệu suất.
Sự khác biệt thực tế giữa hai câu lệnh này là càng mạnh hơn emplace_back
sẽ gọi bất kỳ loại hàm tạo nào ngoài đó, trong khi đó, thận trọng hơn push_back
sẽ chỉ gọi các hàm tạo là ẩn. Các nhà xây dựng tiềm ẩn được cho là an toàn. Nếu bạn có thể ngầm định xây dựng một U
từ a T
, bạn đang nói rằng U
có thể giữ tất cả các thông tin T
mà không bị mất. Nó là an toàn trong hầu hết mọi tình huống để vượt qua T
và không ai sẽ bận tâm nếu bạn U
thay thế nó. Một ví dụ điển hình của hàm tạo ẩn là chuyển đổi từ std::uint32_t
sang std::uint64_t
. Một ví dụ xấu của một chuyển đổi ngầm là double
để std::uint8_t
.
Chúng tôi muốn thận trọng trong lập trình của chúng tôi. Chúng tôi không muốn sử dụng các tính năng mạnh mẽ bởi vì tính năng này càng mạnh thì càng dễ vô tình làm điều gì đó không chính xác hoặc bất ngờ. Nếu bạn có ý định gọi các nhà xây dựng rõ ràng, thì bạn cần sức mạnh của emplace_back
. Nếu bạn chỉ muốn gọi các nhà xây dựng ngầm, hãy gắn bó với sự an toàn của push_back
.
Một ví dụ
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>
có một hàm tạo rõ ràng từ T *
. Bởi vì emplace_back
có thể gọi các hàm tạo rõ ràng, việc truyền một con trỏ không sở hữu sẽ biên dịch tốt. Tuy nhiên, khi v
đi ra khỏi phạm vi, hàm hủy sẽ cố gắng gọi delete
con trỏ đó, thứ không được cấp phát bởi new
vì nó chỉ là một đối tượng ngăn xếp. Điều này dẫn đến hành vi không xác định.
Đây không chỉ là mã được phát minh. Đây là một lỗi sản xuất thực sự tôi gặp phải. Mã là std::vector<T *>
, nhưng nó sở hữu nội dung. Là một phần của việc chuyển đổi sang C ++ 11, tôi đã thay đổi một cách chính xác T *
để std::unique_ptr<T>
chỉ ra rằng các vector sở hữu bộ nhớ của nó. Tuy nhiên, tôi đã dựa trên những thay đổi này vào năm 2012, trong thời gian đó tôi đã nghĩ rằng "emplace_back làm mọi thứ mà Push_back có thể làm và hơn thế nữa, vậy tại sao tôi lại sử dụng Push_back?", Vì vậy tôi cũng thay đổi push_back
thành emplace_back
.
Thay vào đó, nếu tôi để lại mã như sử dụng an toàn hơn push_back
, tôi sẽ ngay lập tức bắt gặp lỗi lâu đời này và nó đã được xem là một thành công của việc nâng cấp lên C ++ 11. Thay vào đó, tôi che dấu lỗi và không tìm thấy nó cho đến nhiều tháng sau.