C ++ 11 emplace_back trên vectơ <struct>?


87

Hãy xem xét chương trình sau:

#include <string>
#include <vector>

using namespace std;

struct T
{
    int a;
    double b;
    string c;
};

vector<T> V;

int main()
{
    V.emplace_back(42, 3.14, "foo");
}

Nó không hoạt động:

$ g++ -std=gnu++11 ./test.cpp
In file included from /usr/include/c++/4.7/x86_64-linux-gnu/bits/c++allocator.h:34:0,
                 from /usr/include/c++/4.7/bits/allocator.h:48,
                 from /usr/include/c++/4.7/string:43,
                 from ./test.cpp:1:
/usr/include/c++/4.7/ext/new_allocator.h: In instantiation of ‘void __gnu_cxx::new_allocator<_Tp>::construct(_Up*, _Args&& ...) [with _Up = T; _Args = {int, double, const char (&)[4]}; _Tp = T]’:
/usr/include/c++/4.7/bits/alloc_traits.h:253:4:   required from ‘static typename std::enable_if<std::allocator_traits<_Alloc>::__construct_helper<_Tp, _Args>::value, void>::type std::allocator_traits<_Alloc>::_S_construct(_Alloc&, _Tp*, _Args&& ...) [with _Tp = T; _Args = {int, double, const char (&)[4]}; _Alloc = std::allocator<T>; typename std::enable_if<std::allocator_traits<_Alloc>::__construct_helper<_Tp, _Args>::value, void>::type = void]’
/usr/include/c++/4.7/bits/alloc_traits.h:390:4:   required from ‘static void std::allocator_traits<_Alloc>::construct(_Alloc&, _Tp*, _Args&& ...) [with _Tp = T; _Args = {int, double, const char (&)[4]}; _Alloc = std::allocator<T>]’
/usr/include/c++/4.7/bits/vector.tcc:97:6:   required from ‘void std::vector<_Tp, _Alloc>::emplace_back(_Args&& ...) [with _Args = {int, double, const char (&)[4]}; _Tp = T; _Alloc = std::allocator<T>]’
./test.cpp:17:32:   required from here
/usr/include/c++/4.7/ext/new_allocator.h:110:4: error: no matching function for call to ‘T::T(int, double, const char [4])’
/usr/include/c++/4.7/ext/new_allocator.h:110:4: note: candidates are:
./test.cpp:6:8: note: T::T()
./test.cpp:6:8: note:   candidate expects 0 arguments, 3 provided
./test.cpp:6:8: note: T::T(const T&)
./test.cpp:6:8: note:   candidate expects 1 argument, 3 provided
./test.cpp:6:8: note: T::T(T&&)
./test.cpp:6:8: note:   candidate expects 1 argument, 3 provided

Cách chính xác để làm điều này là gì và tại sao?

(Cũng đã thử dấu ngoặc đơn và kép)


4
Điều đó sẽ hoạt động nếu bạn cung cấp một hàm tạo thích hợp.
chris

2
Có cách nào để xây dựng nó tại chỗ với hàm tạo cấu trúc dấu ngoặc nhọn được tạo tự động được sử dụng T t{42,3.14, "foo"}không?
Andrew Tomazos 11/12/12

4
Tôi không nghĩ rằng nó có dạng một hàm tạo. Đó là khởi tạo tổng hợp.
chris


5
Tôi không cố gắng ảnh hưởng đến ý kiến ​​của bạn theo bất kỳ cách nào .. Nhưng trong trường hợp bạn đã không chú ý đến câu hỏi mỏng từ một thời gian .. Câu trả lời được chấp nhận, với sự tôn trọng hoàn toàn đối với người viết, không phải là câu trả lời cho câu hỏi của bạn và có thể gây hiểu lầm cho người đọc.
Humam Helfawi

Câu trả lời:


18

Đối với bất kỳ ai từ tương lai, hành vi này sẽ được thay đổi trong C ++ 20 .

Nói cách khác, mặc dù việc triển khai nội bộ vẫn T(arg0, arg1, ...)sẽ được gọi là nó sẽ được coi là thường xuyên T{arg0, arg1, ...}mà bạn mong đợi.


93

Bạn cần xác định rõ ràng một ctor cho lớp:

#include <string>
#include <vector>

using namespace std;

struct T
{
    int a;
    double b;
    string c;

    T(int a, double b, string &&c) 
        : a(a)
        , b(b)
        , c(std::move(c)) 
    {}
};

vector<T> V;

int main()
{
    V.emplace_back(42, 3.14, "foo");
}

Mục đích của việc sử dụng emplace_backlà tránh tạo một đối tượng tạm thời, đối tượng này sau đó được sao chép (hoặc di chuyển) đến đích. Mặc dù cũng có thể tạo một đối tượng tạm thời, sau đó chuyển đối tượng đó sang emplace_back, nhưng nó sẽ đánh bại (ít nhất là hầu hết) mục đích. Những gì bạn muốn làm là chuyển các đối số riêng lẻ, sau đó cho phép emplace_backgọi ctor với các đối số đó để tạo đối tượng tại chỗ.


12
Tôi nghĩ cách tốt hơn sẽ là viếtT(int a, double b, string c) : a(a), b(b), c(std::move(c))
balki

9
Câu trả lời được chấp nhận đánh bại mục đích của emplace_back. Đây là câu trả lời chính xác. Đây là cách emplace*làm việc. Chúng xây dựng phần tử tại chỗ bằng cách sử dụng các đối số được chuyển tiếp. Do đó, một hàm tạo là cần thiết để nhận các đối số đã nói.
underscore_d

1
tuy nhiên, vectơ có thể cung cấp một emplace_aggr, phải không?
tamas.kenez

@balki Đúng vậy, không có điểm chụp cbởi &&nếu không có gì được thực hiện với rvalueness có thể của nó; tại trình khởi tạo của thành viên, đối số lại được coi là giá trị, trong trường hợp không có ép kiểu, vì vậy thành viên chỉ được tạo bản sao. Ngay cả khi thành viên được xây dựng bằng cách di chuyển, việc yêu cầu người gọi luôn phải vượt qua std::move()giá trị tạm thời hoặc giá trị d (mặc dù tôi sẽ thú nhận rằng tôi có một số trường hợp góc trong mã của mình khi tôi làm điều đó, nhưng chỉ trong chi tiết triển khai) .
underscore_d

25

Tất nhiên, đây không phải là một câu trả lời, nhưng nó cho thấy một tính năng thú vị của bộ giá trị:

#include <string>
#include <tuple>
#include <vector>

using namespace std;

using T = tuple <
    int,
    double,
    string
>;

vector<T> V;

int main()
{
    V.emplace_back(42, 3.14, "foo");
}

8
Nó chắc chắn không phải là một câu trả lời. có gì thú vị về nó? bất kỳ loại nào có ctor đều có thể được thay thế theo cách này. tuple có một ctor. cấu trúc của op đã không. đó là câu trả lời.
underscore_d

6
@underscore_d: Tôi không chắc mình nhớ mọi chi tiết về những gì tôi đã nghĩ cách đây 3 năm rưỡi, nhưng tôi biết điều tôi đang đề xuất là nếu bạn chỉ sử dụng một tuplethay vì xác định cấu trúc POD, thì bạn sẽ nhận được một hàm tạo miễn phí , có nghĩa là bạn nhận được emplacecú pháp miễn phí (trong số những thứ khác - bạn cũng nhận được thứ tự từ vựng). Bạn mất tên thành viên, nhưng đôi khi việc tạo người truy cập ít bận tâm hơn tất cả phần còn lại của bảng soạn sẵn mà bạn cần. Tôi đồng ý rằng câu trả lời của Jerry Coffin tốt hơn nhiều so với câu được chấp nhận. Tôi cũng đã ủng hộ nó từ nhiều năm trước.
rici

3
Vâng, chính tả nó giúp tôi hiểu ý bạn! Điểm tốt. Tôi đồng ý rằng đôi khi sự khái quát hóa có thể chấp nhận được khi cân nhắc với những thứ khác mà STL cung cấp cho chúng ta: Tôi sử dụng nó thường xuyên với pair... nhưng đôi khi tự hỏi liệu tôi có thực sự thu được nhiều lợi nhuận về mặt ròng không, heh. Nhưng có lẽ tuplesẽ theo sau trong tương lai. Cảm ơn vì đã mở rộng!
underscore_d

12

Nếu bạn không muốn (hoặc không thể) thêm một hàm tạo, hãy chuyên biệt hóa bộ cấp phát cho T (hoặc tạo một bộ cấp phát của riêng bạn).

namespace std {
    template<>
    struct allocator<T> {
        typedef T value_type;
        value_type* allocate(size_t n) { return static_cast<value_type*>(::operator new(sizeof(value_type) * n)); }
        void deallocate(value_type* p, size_t n) { return ::operator delete(static_cast<void*>(p)); }
        template<class U, class... Args>
        void construct(U* p, Args&&... args) { ::new(static_cast<void*>(p)) U{ std::forward<Args>(args)... }; }
    };
}

Lưu ý: Cấu trúc hàm thành viên hiển thị ở trên không thể biên dịch với clang 3.1 (Xin lỗi, tôi không biết tại sao). Hãy thử cách tiếp theo nếu bạn sử dụng clang 3.1 (hoặc các lý do khác).

void construct(T* p, int a, double b, const string& c) { ::new(static_cast<void*>(p)) T{ a, b, c }; }

Trong chức năng phân bổ của bạn, bạn không cần phải lo lắng về sự liên kết? Xemstd::aligned_storage
Andrew Tomazos 13/12/12

Không vấn đề gì. Theo đặc điểm kỹ thuật, hiệu ứng của "void * :: operator new (size_t size)" là "Hàm cấp phát được gọi bởi một biểu thức mới để phân bổ kích thước byte lưu trữ được căn chỉnh phù hợp để đại diện cho bất kỳ đối tượng nào có kích thước đó."
Mitsuru Kariya

6

Điều này dường như được đề cập trong 23.2.1 / 13.

Đầu tiên, định nghĩa:

Cho một loại vùng chứa X có kiểu cấp phát giống với A và kiểu giá trị giống với T và được cung cấp giá trị m thuộc kiểu A, con trỏ p kiểu T *, biểu thức v thuộc kiểu T và giá trị rv thuộc kiểu T, các thuật ngữ sau được xác định.

Bây giờ, điều gì làm cho nó có thể xây dựng được:

T là EmplaceConstructible thành X từ args, đối với không hoặc nhiều đối số args, có nghĩa là biểu thức sau được định dạng tốt: certator_traits :: construct (m, p, args);

Và cuối cùng là một lưu ý về việc triển khai mặc định của lệnh gọi cấu trúc:

Lưu ý: Một vùng chứa gọi phân bổ phân bổ_traits :: construct (m, p, args) để xây dựng một phần tử tại p bằng cách sử dụng args. Cấu trúc mặc định trong std :: phân bổ sẽ gọi :: new ((void *) p) T (args), nhưng các trình cấp phát chuyên biệt có thể chọn một định nghĩa khác.

Điều này cho chúng ta biết khá nhiều rằng đối với một lược đồ trình cấp phát mặc định (và có thể là duy nhất), bạn phải xác định một hàm tạo với số lượng đối số thích hợp cho thứ mà bạn đang cố gắng cho phép xây dựng vào một vùng chứa.


-2

bạn phải xác định một phương thức khởi tạo cho kiểu của bạn Tvì nó chứa một phương thức std::stringkhông tầm thường.

hơn nữa, sẽ tốt hơn nếu xác định (có thể mặc định) di chuyển ctor / gán (vì bạn có một std::stringthành viên có thể di chuyển ) - điều này sẽ giúp di chuyển của bạn Thiệu quả hơn nhiều ...

hoặc, chỉ sử dụng T{...}để gọi quá tải emplace_back()như được khuyến nghị trong phản hồi hàng xóm ... mọi thứ phụ thuộc vào các trường hợp sử dụng điển hình của bạn ...


Một hàm tạo di chuyển được tạo tự động cho T
Andrew Tomazos.

1
@ AndrewTomazos-Fathomling: chỉ khi không có ctors người dùng nào được xác định
zaufi 11/12/12

1
Đúng, và họ không.
Andrew Tomazos

@ AndrewTomazos-Fathomling: nhưng bạn phải xác định một số người, để tránh trường hợp tạm thời trên emplace_back()cuộc gọi :)
zaufi

1
Trên thực tế không chính xác. Một hàm tạo di chuyển được tạo tự động với điều kiện không có hàm hủy, hàm tạo sao chép hoặc toán tử gán nào được xác định. Việc xác định một phương thức khởi tạo thành viên 3 đối số để sử dụng với emplace_back sẽ không áp dụng phương thức khởi tạo di chuyển mặc định.
Andrew Tomazos 11/12/12

-2

Bạn có thể tạo struct Tphiên bản và sau đó di chuyển nó vào vector:

V.push_back(std::move(T {42, 3.14, "foo"}));

2
Bạn không cần phải std :: move () một đối tượng tạm thời T {...}. Nó đã là một đối tượng tạm thời (rvalue). Vì vậy, bạn có thể bỏ std :: move () từ ví dụ của bạn.
Nadav Har'El

Hơn nữa, ngay cả tên kiểu T cũng không cần thiết - trình biên dịch có thể đoán nó. Vì vậy, chỉ cần "V.push_back {42, 3.14," foo "}" sẽ hoạt động.
Nadav Har'El

-8

Bạn có thể sử dụng {}cú pháp để khởi tạo phần tử mới:

V.emplace_back(T{42, 3.14, "foo"});

Điều này có thể được tối ưu hóa hoặc không, nhưng nó nên được.

Bạn phải xác định một hàm tạo để điều này hoạt động, lưu ý rằng với mã của bạn, bạn thậm chí không thể làm được:

T a(42, 3.14, "foo");

Nhưng đây là những gì bạn cần để có công việc thay thế.

vì vậy chỉ cần:

struct T { 
  ...
  T(int a_, double b_, string c_) a(a_), b(b_), c(c_) {}
}

sẽ làm cho nó hoạt động theo cách mong muốn.


10
Điều này sẽ xây dựng một tạm thời và sau đó di chuyển cấu trúc đó vào mảng? - hay nó sẽ xây dựng mục tại chỗ?
Andrew Tomazos 11/12/12

3
Các std::movelà không cần thiết. T{42, 3.14, "foo"}sẽ được chuyển tiếp bởi emplace_back và liên kết với hàm tạo struct move dưới dạng rvalue. Tuy nhiên, tôi thích một giải pháp xây dựng nó tại chỗ.
Andrew Tomazos 11/12/12

37
trong trường hợp này, việc di chuyển gần như chính xác tương đương với sao chép, vì vậy toàn bộ điểm trống bị bỏ sót.
Alex I.

5
@AlexI. Thật! Cú pháp này tạo ra một tạm thời, được chuyển làm đối số cho 'emplace_back'. Hoàn toàn bỏ sót điểm.
aldo

5
Tôi không hiểu tất cả các phản hồi tiêu cực. Trình biên dịch sẽ không sử dụng RVO trong trường hợp này?
Euri Pinhollow
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.