Tại sao tôi lại sử dụng Push_back thay vì emplace_back?


232

Các vectơ C ++ 11 có chức năng mới emplace_back. Không giống như push_back, dựa trên tối ưu hóa trình biên dịch để tránh các bản sao, emplace_backsử dụng chuyển tiếp hoàn hảo để gửi các đối số trực tiếp đến hàm tạo để tạo một đối tượng tại chỗ. Dường như với tôi rằng emplace_backmọi thứ đều push_backcó thể làm được, nhưng đôi khi nó sẽ làm điều đó tốt hơn (nhưng không bao giờ tệ hơn).

Lý do nào tôi phải sử dụng push_back?

Câu trả lời:


162

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_backvs emplace_backbỏ 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_backso với emplace_backlú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_backhoặ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_backsẽ xây dựng một đối tượng tạm thời, sau đó sẽ được chuyển vào vtrong khi emplace_backsẽ 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_backsẽ gọi bất kỳ loại hàm tạo nào ngoài đó, trong khi đó, thận trọng hơn push_backsẽ 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 Utừ a T, bạn đang nói rằng Ucó thể giữ tất cả các thông tin Tmà không bị mất. Nó là an toàn trong hầu hết mọi tình huống để vượt qua Tvà không ai sẽ bận tâm nếu bạn Uthay thế nó. Một ví dụ điển hình của hàm tạo ẩn là chuyển đổi từ std::uint32_tsang 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_backcó 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 deletecon trỏ đó, thứ không được cấp phát bởi newvì 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_backthà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.


Nó sẽ giúp ích nếu bạn có thể giải thích chính xác những gì emplace làm trong ví dụ của bạn và tại sao nó sai.
eddi

4
@eddi: Tôi đã thêm một phần giải thích điều này: std::unique_ptr<T>có một hàm tạo rõ ràng từ T *. Bởi vì emplace_backcó 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 deletecon trỏ đó, thứ không được cấp phát bởi newvì 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.
David Stone

Cảm ơn đã đăng bài này. Tôi đã không biết về nó khi tôi viết câu trả lời của mình nhưng bây giờ tôi ước mình đã tự viết nó khi tôi học nó sau đó :) Tôi thực sự muốn tát những người chuyển sang các tính năng mới chỉ để làm điều hippest họ có thể tìm thấy . Các bạn, mọi người cũng đang sử dụng C ++ trước C ++ 11, và không phải mọi thứ về nó đều có vấn đề. Nếu bạn không biết tại sao bạn đang sử dụng một tính năng, đừng sử dụng nó . Rất vui vì bạn đã đăng bài này và tôi hy vọng nó sẽ được nhiều người ủng hộ hơn để nó vượt lên trên tôi. +1
dùng541686

1
@CaptainJacksparrow: Có vẻ như tôi nói ngầm và rõ ràng nơi tôi muốn nói đến họ. Phần nào làm bạn bối rối?
David Stone

4
@CaptainJacksparrow: Một explicitconstructor là một constructor có từ khóa explicitđược áp dụng cho nó. Hàm tạo "ẩn" là bất kỳ hàm tạo nào không có từ khóa đó. Trong trường hợp của hàm std::unique_ptrtạo từ T *, người triển khai std::unique_ptrđã viết hàm tạo đó, nhưng vấn đề ở đây là người dùng của kiểu đó được gọi emplace_back, gọi là hàm tạo rõ ràng đó. Nếu có push_back, thay vì gọi hàm tạo đó, nó sẽ dựa vào một chuyển đổi ngầm, chỉ có thể gọi các hàm tạo ẩn.
David Stone

117

push_backluôn cho phép sử dụng khởi tạo thống nhất, điều mà tôi rất thích. Ví dụ:

struct aggregate {
    int foo;
    int bar;
};

std::vector<aggregate> v;
v.push_back({ 42, 121 });

Mặt khác, v.emplace_back({ 42, 121 });sẽ không hoạt động.


58
Lưu ý rằng điều này chỉ áp dụng cho khởi tạo tổng hợp và khởi tạo danh sách khởi tạo. Nếu bạn sẽ sử dụng {}cú pháp để gọi một nhà xây dựng thực tế, thì bạn chỉ cần loại bỏ {}và sử dụng emplace_back.
Nicol Bolas

Thời gian câu hỏi ngớ ngẩn: vì vậy emplace_back không thể được sử dụng cho các vectơ cấu trúc? Hoặc chỉ không cho phong cách này bằng cách sử dụng chữ {42.121}?
Phil H

1
@LucDanton: Như tôi đã nói, nó chỉ áp dụng cho khởi tạo danh sách tổng hợpkhởi tạo. Bạn có thể sử dụng {}cú pháp để gọi các nhà xây dựng thực tế. Bạn có thể đưa ra aggregatemột hàm tạo có 2 số nguyên và hàm tạo này sẽ được gọi khi sử dụng {}cú pháp. Vấn đề là nếu bạn đang cố gắng gọi một nhà xây dựng, emplace_backsẽ tốt hơn, vì nó gọi nhà xây dựng tại chỗ. Và do đó không yêu cầu loại phải có thể sao chép.
Nicol Bolas

11
Điều này đã được xem như là một khiếm khuyết trong tiêu chuẩn, và đã được giải quyết. Xem cplusplus.github.io/LWG/lwg-active.html#2089
David Stone

3
@DavidStone Đã được giải quyết, nó sẽ vẫn không nằm trong danh sách "hoạt động" ... không? Nó dường như vẫn là một vấn đề nổi bật. Bản cập nhật mới nhất, đứng đầu " [2018-08-23 Batavia Xử lý các vấn đề] ", nói rằng " P0960 (hiện đang trong chuyến bay) sẽ giải quyết vấn đề này. " Và tôi vẫn không thể biên dịch mã cố gắng emplacetổng hợp mà không viết rõ ràng một công cụ xây dựng nồi hơi . Tại thời điểm này, vẫn chưa rõ liệu nó sẽ được coi là một khiếm khuyết và do đó đủ điều kiện để nhập lại hay liệu người dùng C ++ <20 sẽ vẫn là SoL.
gạch dưới

80

Khả năng tương thích ngược với trình biên dịch trước C ++ 11.


21
Đó dường như là lời nguyền của C ++. Chúng tôi nhận được vô số tính năng thú vị với mỗi bản phát hành mới, nhưng rất nhiều công ty bị mắc kẹt khi sử dụng một số phiên bản cũ vì mục đích tương thích hoặc không khuyến khích sử dụng một số tính năng nhất định.
Dan Albert

6
@Mehrdad: Tại sao giải quyết đủ khi bạn có thể có tuyệt vời? Tôi chắc chắn sẽ không muốn lập trình trong blub , ngay cả khi nó là đủ. Không nói đó là trường hợp cụ thể cho ví dụ này, nhưng như một người dành phần lớn thời gian của mình để lập trình trong C89 vì mục đích tương thích, đó chắc chắn là một vấn đề thực sự.
Dan Albert

3
Tôi không nghĩ rằng đây thực sự là một câu trả lời cho câu hỏi. Đối với tôi, anh ấy yêu cầu sử dụng - trường hợp push_backthích hợp hơn.
Cậu bé

3
@ Mr.Boy: Tốt hơn là khi bạn muốn tương thích ngược với trình biên dịch trước C ++ 11. Điều đó không rõ ràng trong câu trả lời của tôi?
dùng541686

6
Điều này đã nhận được sự chú ý cách nhiều hơn tôi đã mong đợi, vì vậy đối với tất cả các bạn đọc bài viết này: emplace_backkhông một phiên bản "vĩ đại" của push_back. Đây là một phiên bản nguy hiểm tiềm tàng của nó. Đọc các câu trả lời khác.
dùng541686

68

Một số triển khai thư viện của emplace_back không hoạt động như được chỉ định trong tiêu chuẩn C ++, bao gồm cả phiên bản đi kèm với Visual Studio 2012, 2013 và 2015.

Để phù hợp với các lỗi trình biên dịch đã biết, ưu tiên sử dụng std::vector::push_back()nếu các tham số lặp tham số hoặc các đối tượng khác sẽ không hợp lệ sau cuộc gọi.

std::vector<int> v;
v.emplace_back(123);
v.emplace_back(v[0]); // Produces incorrect results in some compilers

Trên một trình biên dịch, v chứa các giá trị 123 và 21 thay vì 123 và 123 dự kiến. Điều này là do thực tế là lệnh gọi thứ 2 dẫn đến emplace_backthay đổi kích thước tại điểm v[0]trở nên không hợp lệ.

Việc triển khai mã trên sẽ sử dụng push_back()thay vì emplace_back()như sau:

std::vector<int> v;
v.emplace_back(123);
v.push_back(v[0]);

Lưu ý: Việc sử dụng một vectơ int là cho mục đích trình diễn. Tôi đã phát hiện ra vấn đề này với một lớp phức tạp hơn nhiều bao gồm các biến thành viên được phân bổ động và lời kêu gọi emplace_back()dẫn đến một sự cố khó khăn.


Chờ đợi. Điều này dường như là một lỗi. Làm thế nào có push_backthể khác nhau trong trường hợp này?
Balki

12
Cuộc gọi đến emplace_back () sử dụng chuyển tiếp hoàn hảo để thực hiện xây dựng tại chỗ và do đó v [0] không được đánh giá cho đến khi vectơ được thay đổi kích thước (tại đó v [0] không hợp lệ). Push_back xây dựng phần tử mới và sao chép / di chuyển phần tử khi cần và v [0] được ước tính trước bất kỳ sự phân bổ lại nào.
Marc

1
@Marc: Nó được đảm bảo bởi tiêu chuẩn mà emplace_back hoạt động ngay cả đối với các thành phần trong phạm vi.
David Stone

1
@DavidStone: Vui lòng cung cấp tài liệu tham khảo về nơi mà tiêu chuẩn hành vi này được đảm bảo? Dù bằng cách nào, Visual Studio 2012 và 2015 thể hiện hành vi không chính xác.
Marc

4
@cameino: emplace_back tồn tại để trì hoãn việc đánh giá tham số của nó để giảm việc sao chép không cần thiết. Hành vi này không được xác định hoặc lỗi trình biên dịch (phân tích chờ xử lý của tiêu chuẩn). Gần đây tôi đã chạy thử nghiệm tương tự với Visual Studio 2015 và nhận được 123,3 trong Phiên bản x64, 123,40 trong Phiên bản Win32 và 123, -572662307 trong Debug x64 và Debug Win32.
Marc
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.