C ++ 11 emplace_back pada vektor <struct>?

89

Pertimbangkan program berikut:

#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");
}

Tidak berhasil:

$ 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

Apa cara yang benar untuk melakukan ini dan mengapa?

(Juga mencoba kawat gigi tunggal dan ganda)

Andrew Tomazos
sumber
4
Itu akan berfungsi jika Anda menyediakan konstruktor yang sesuai.
chris
3
Apakah ada cara untuk membangunnya dengan konstruktor struct brace yang dibuat secara otomatis yang digunakan oleh T t{42,3.14, "foo"}?
Andrew Tomazos
4
Saya tidak berpikir itu mengambil bentuk konstruktor. Ini adalah inisialisasi agregat.
chris
5
Saya tidak mencoba mempengaruhi pendapat Anda dengan cara apa pun .. Tetapi jika Anda tidak memberikan perhatian pada pertanyaan tipis dari beberapa saat .. Jawaban yang diterima, dengan hormat penuh kepada penulisnya, bukanlah jawaban sama sekali untuk pertanyaan Anda dan mungkin menyesatkan pembaca.
Humam Helfawi

Jawaban:

19

Untuk siapa pun di masa mendatang, perilaku ini akan diubah di C ++ 20 .

Dengan kata lain, meskipun implementasi secara internal akan tetap memanggil T(arg0, arg1, ...)itu akan dianggap biasa seperti T{arg0, arg1, ...}yang Anda harapkan.

XIII merah
sumber
97

Anda perlu secara eksplisit mendefinisikan ctor untuk kelas:

#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");
}

Tujuan penggunaannya emplace_backadalah untuk menghindari pembuatan objek sementara, yang kemudian disalin (atau dipindahkan) ke tujuan. Meskipun dimungkinkan juga untuk membuat objek sementara, lalu meneruskannya ke emplace_back, itu mengalahkan (setidaknya sebagian besar) tujuannya. Apa yang ingin Anda lakukan adalah meneruskan argumen individual, lalu biarkan emplace_backmemanggil ctor dengan argumen tersebut untuk membuat objek pada tempatnya.

Jerry Coffin
sumber
12
Saya pikir cara yang lebih baik adalah menulisT(int a, double b, string c) : a(a), b(b), c(std::move(c))
balki
9
Jawaban yang diterima mengalahkan tujuan dari emplace_back. Ini jawaban yang benar. Beginilah cara emplace*kerjanya. Mereka membangun elemen di tempat menggunakan argumen yang diteruskan. Oleh karena itu, diperlukan seorang konstruktor untuk mengambil argumen tersebut.
underscore_d
1
tetap saja, vektor bisa memberikan emplace_aggr, bukan?
tamas.kenez
@balki Benar, tidak ada gunanya mengambil cdengan &&jika tidak dilakukan dengan rvalueness yang mungkin terjadi; pada penginisialisasi anggota, argumen diperlakukan sebagai nilai l lagi, jika tidak ada pemeran, sehingga anggota hanya mendapatkan salinan-konstruksi. Sekalipun anggota itu dibuat-pindah, bukanlah idiomatis untuk meminta penelepon selalu memberikan nilai sementara atau std::move()d l (meskipun saya akan mengakui bahwa saya memiliki beberapa kasus sudut dalam kode saya di mana saya melakukan itu, tetapi hanya dalam detail implementasi) .
underscore_d
26

Tentu saja, ini bukan jawaban, tetapi ini menunjukkan fitur tupel yang menarik:

#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");
}
rici
sumber
9
Ini pasti bukan jawaban. apa yang menarik dari itu? tipe apapun dengan ctor dapat ditempatkan dengan cara ini. tuple memiliki ctor. struktur operasi tidak. itulah jawabannya.
underscore_d
6
@underscore_d: Saya tidak yakin bahwa saya mengingat setiap detail dari apa yang saya pikirkan 3½ tahun yang lalu, tetapi saya pikir apa yang saya sarankan adalah jika Anda hanya menggunakan tuplealih - alih mendefinisikan struct POD, maka Anda mendapatkan konstruktor secara gratis , yang berarti Anda mendapatkan emplacesintaks secara gratis (antara lain - Anda juga mendapatkan pengurutan leksikografik). Anda kehilangan nama anggota, tapi terkadang tidak terlalu repot membuat pengakses daripada semua boilerplate lainnya yang seharusnya Anda butuhkan. Saya setuju bahwa jawaban Jerry Coffin jauh lebih baik daripada yang diterima. Saya juga memilihnya beberapa tahun lalu.
rici
3
Ya, mengejanya membantu saya memahami apa yang Anda maksud! Poin yang bagus. Saya setuju bahwa kadang-kadang generalisasi dapat diterima ketika dibandingkan dengan hal-hal lain yang disediakan STL untuk kita: Saya sering menggunakannya dengan pair... tetapi kadang-kadang bertanya-tanya apakah saya benar-benar mendapatkan banyak keuntungan secara net, heh. Tapi mungkin tupleakan menyusul di masa depan. Terima kasih telah berkembang!
underscore_d
12

Jika Anda tidak ingin (atau tidak dapat) menambahkan konstruktor, mengkhususkan pengalokasi untuk T (atau buat pengalokasi Anda sendiri).

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)... }; }
    };
}

Catatan: Konstruksi fungsi anggota yang ditunjukkan di atas tidak dapat dikompilasi dengan clang 3.1 (Maaf, saya tidak tahu mengapa). Coba yang berikutnya jika Anda akan menggunakan dentang 3.1 (atau alasan lain).

void construct(T* p, int a, double b, const string& c) { ::new(static_cast<void*>(p)) T{ a, b, c }; }
Mitsuru Kariya
sumber
Dalam fungsi alokasi Anda, apakah Anda tidak perlu khawatir tentang penyelarasan? Lihatstd::aligned_storage
Andrew Tomazos
Tidak masalah. Menurut spesifikasi, efek dari "void * :: operator new (size_t size)" adalah "Fungsi alokasi yang dipanggil oleh ekspresi baru untuk mengalokasikan ukuran byte penyimpanan yang diselaraskan dengan tepat untuk mewakili objek dengan ukuran itu."
Mitsuru Kariya
6

Ini sepertinya tercakup dalam 23.2.1 / 13.

Pertama, definisi:

Diberikan sebuah wadah tipe X yang memiliki sebuah tipe_alokasi identik dengan A dan sebuah nilai_jenis identik dengan T dan diberi nilai m tipe A, sebuah penunjuk p tipe T *, ekspresi v tipe T, dan nilai rv tipe T, istilah berikut didefinisikan.

Sekarang, apa yang membuatnya bisa dibangun dengan emplace:

T adalah EmplaceConstructible ke dalam X dari args, untuk nol atau lebih argumen, berarti ekspresi berikut dibentuk dengan baik: alokator_traits :: konstruk (m, p, args);

Dan terakhir catatan tentang implementasi default dari panggilan konstruksi:

Catatan: Sebuah wadah memanggil alokator_traits :: konstruk (m, p, args) untuk membangun sebuah elemen di p menggunakan args. Konstruksi default di std :: alokator akan memanggil :: new ((void *) p) T (args), tetapi pengalokasi khusus dapat memilih definisi yang berbeda.

Ini cukup banyak memberi tahu kita bahwa untuk skema pengalokasi default (dan kemungkinan satu-satunya) Anda harus telah mendefinisikan konstruktor dengan jumlah argumen yang tepat untuk hal yang Anda coba tempatkan-bangun ke dalam wadah.

Mark B
sumber
-2

Anda harus menentukan konstruktor untuk tipe Anda Tkarena mengandung std::stringyang tidak sepele.

Selain itu, akan lebih baik untuk mendefinisikan (kemungkinan default) pindah ctor / assign (karena Anda memiliki yang dapat dipindahkan std::stringsebagai anggota) - ini akan membantu untuk memindahkan Anda Tjauh lebih efisien ...

atau, cukup gunakan T{...}untuk panggilan yang kelebihan beban emplace_back()seperti yang direkomendasikan dalam respons tetangga ... semuanya tergantung pada kasus penggunaan khas Anda ...

zaufi
sumber
Konstruktor bergerak secara otomatis dibuat untuk T
Andrew Tomazos
1
@ AndrewTomazos-Fathomling: hanya jika tidak ada pemberi pengguna yang ditentukan
zaufi
1
Benar, dan ternyata tidak.
Andrew Tomazos
@ AndrewTomazos-Fathomling: tetapi Anda harus menentukan beberapa, untuk menghindari contoh sementara saat emplace_back()panggilan :)
zaufi
1
Sebenarnya salah. Konstruktor pemindahan secara otomatis dibuat asalkan tidak ada destruktor, konstruktor salinan atau operator penugasan yang ditentukan. Mendefinisikan konstruktor 3 argumen memberwise untuk digunakan dengan emplace_back tidak akan menekan konstruktor pemindahan default.
Andrew Tomazos
-2

Anda dapat membuat struct Tinstance dan kemudian memindahkannya ke vektor:

V.push_back(std::move(T {42, 3.14, "foo"}));
AlexB
sumber
2
Anda tidak perlu std :: move () objek sementara T {...}. Ini sudah menjadi objek sementara (rvalue). Jadi Anda bisa melepaskan std :: move () dari contoh Anda.
Nadav Har'El
Selain itu, bahkan nama tipe T tidak diperlukan - kompilator dapat menebaknya. Jadi, hanya "V.push_back {42, 3.14," foo "}" yang akan bekerja.
Nadav Har'El
-8

Anda dapat menggunakan {}sintaks untuk menginisialisasi elemen baru:

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

Ini mungkin atau mungkin tidak dioptimalkan, tetapi seharusnya dioptimalkan.

Anda harus menentukan konstruktor agar ini berfungsi, perhatikan bahwa dengan kode Anda, Anda bahkan tidak dapat melakukan:

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

Tapi inilah yang Anda butuhkan untuk memiliki pekerjaan emplace.

jadi hanya:

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

akan membuatnya bekerja dengan cara yang diinginkan.

nyata
sumber
10
Apakah ini akan membangun sementara dan kemudian memindahkannya ke dalam array? - atau akankah membangun item di tempat?
Andrew Tomazos
3
Tidak std::moveperlu. T{42, 3.14, "foo"}sudah akan diteruskan oleh emplace_back dan mengikat ke struct move constructor sebagai rvalue. Namun saya lebih suka solusi yang membangunnya di tempat.
Andrew Tomazos
37
dalam hal ini pemindahan hampir persis sama dengan menyalin, sehingga seluruh titik emplace terlewatkan.
Alex I.
5
@Lihatmodelseksi Memang! Sintaks ini membuat sementara, yang diteruskan sebagai argumen ke 'emplace_back'. Benar-benar melenceng.
aldo
5
Saya tidak mengerti semua umpan balik negatif. Tidak akan kompiler menggunakan RVO dalam kasus ini?
Euri Pinhollow