Apakah aman untuk push_back elemen dari vektor yang sama?

126
vector<int> v;
v.push_back(1);
v.push_back(v[0]);

Jika push_back kedua menyebabkan realokasi, referensi ke integer pertama dalam vektor tidak akan lagi valid. Jadi ini tidak aman?

vector<int> v;
v.push_back(1);
v.reserve(v.size() + 1);
v.push_back(v[0]);

Ini membuatnya aman?

Neil Kirk
sumber
4
Catatan: Saat ini ada diskusi di forum proposal standar. Sebagai bagian dari itu, seseorang memberi contoh implementasipush_back . Poster lain mencatat ada bug di dalamnya , sehingga tidak menangani case yang Anda jelaskan dengan benar. Tidak ada orang lain, sejauh yang saya tahu, berpendapat bahwa ini bukan bug. Bukan mengatakan itu bukti konklusif, hanya sebuah pengamatan.
Benjamin Lindley
9
Maaf, saya tidak tahu jawaban mana yang harus diterima karena masih ada kontroversi atas jawaban yang benar.
Neil Kirk
4
Saya diminta untuk mengomentari pertanyaan ini dengan komentar ke-5 di bawah: stackoverflow.com/a/18647445/576911 . Saya melakukannya dengan memutakhirkan setiap jawaban yang saat ini mengatakan: ya, aman untuk push_back elemen dari vektor yang sama.
Howard Hinnant
2
@BenVoigt: <shrug> Jika Anda tidak setuju dengan apa yang dikatakan standar, atau bahkan jika Anda setuju dengan standar, tetapi jangan berpikir itu mengatakannya dengan cukup jelas, ini selalu menjadi pilihan untuk Anda: cplusplus.github.io/LWG/ lwg-active.html # submit_issue Saya telah mengambil opsi ini sendiri lebih dari yang bisa saya ingat. Terkadang berhasil, kadang tidak. Jika Anda ingin memperdebatkan apa yang dikatakan standar, atau apa yang seharusnya dikatakan, SO bukanlah forum yang efektif. Percakapan kami tidak memiliki makna normatif. Tetapi Anda dapat memiliki peluang pada dampak normatif dengan mengikuti tautan di atas.
Howard Hinnant
2
@ Polaris878 Jika push_back menyebabkan vektor mencapai kapasitasnya, vektor akan mengalokasikan buffer yang lebih besar, menyalin data lama, dan kemudian menghapus buffer lama. Kemudian akan memasukkan elemen baru. Masalahnya adalah, elemen baru adalah referensi ke data di buffer lama yang baru saja dihapus. Kecuali push_back membuat salinan nilai sebelum menghapus, itu akan menjadi referensi yang buruk.
Neil Kirk

Jawaban:

31

Sepertinya http://www.open-std.org/jtc1/sc22/wg21/docs/lwg-closed.html#526 mengatasi masalah ini (atau sesuatu yang sangat mirip dengannya) sebagai kemungkinan cacat pada standar:

1) Parameter yang diambil oleh referensi const dapat diubah selama pelaksanaan fungsi

Contoh:

Diberi std :: vector v:

v.insert (v.begin (), v [2]);

v [2] dapat diubah dengan menggerakkan elemen vektor

Resolusi yang diusulkan adalah bahwa ini bukan cacat:

vector :: insert (iter, value) diperlukan untuk bekerja karena standar tidak memberikan izin agar tidak berfungsi.

Nate Kohl
sumber
Saya menemukan izin di 17.6.4.9: "Jika argumen ke suatu fungsi memiliki nilai yang tidak valid (seperti nilai di luar domain fungsi atau pointer yang tidak valid untuk penggunaan yang dimaksudkan), perilaku tidak terdefinisi." Jika realokasi terjadi, maka semua iterator dan referensi ke elemen tidak valid, artinya referensi parameter yang diteruskan ke fungsi juga tidak valid.
Ben Voigt
4
Saya pikir intinya adalah implementasi bertanggung jawab untuk melakukan realokasi. Merupakan kewajiban untuk memastikan perilaku didefinisikan jika input awalnya didefinisikan. Karena spesifikasi dengan jelas menentukan bahwa push_back membuat salinan, implementasi harus, dengan mengorbankan waktu pelaksanaan mungkin, cache atau salin semua nilai sebelum tidak mengalokasikan. Karena dalam pertanyaan khusus ini tidak ada referensi eksternal yang tersisa, tidak masalah jika iterator dan referensi dibatalkan.
OlivierD
3
@NeilKirk Saya pikir ini harus menjadi jawaban otoritatif, juga disebutkan oleh Stephan T. Lavavej pada Reddit menggunakan argumen yang pada dasarnya sama.
TemplateRex
v.insert(v.begin(), v[2]);tidak dapat memicu realokasi. Jadi bagaimana ini menjawab pertanyaan?
ThomasMcLeod
@ThomasMcLeod: ya itu jelas dapat memicu realokasi. Anda memperluas ukuran vektor dengan memasukkan elemen baru.
Violet Giraffe
21

Ya, itu aman, dan implementasi perpustakaan standar melompat melalui lingkaran untuk membuatnya begitu.

Saya percaya pelaksana melacak persyaratan ini kembali ke 23.2 / 11 entah bagaimana, tapi saya tidak tahu caranya, dan saya juga tidak bisa menemukan sesuatu yang lebih konkret. Yang terbaik yang bisa saya temukan adalah artikel ini:

http://www.drdobbs.com/cpp/copying-container-elements-from-the-c-li/240155771

Pemeriksaan implementasi libc ++ dan libstdc ++ menunjukkan bahwa mereka juga aman.

Sebastian Redl
sumber
9
Beberapa dukungan akan sangat membantu di sini.
chris
3
Itu menarik, harus saya akui saya tidak pernah mempertimbangkan kasusnya tetapi memang sepertinya itu cukup sulit untuk dicapai. Apakah itu berlaku untuk vec.insert(vec.end(), vec.begin(), vec.end());?
Matthieu M.
2
@ MatthieuM. Tidak: Tabel 100 mengatakan: "pre: i dan j bukan iterators menjadi a".
Sebastian Redl
2
Saya tidak setuju sekarang karena ini adalah ingatan saya juga, tetapi referensi diperlukan.
bames53
3
Apakah 23.2 / 11 dalam versi yang Anda gunakan "Kecuali dinyatakan sebaliknya (baik secara eksplisit atau dengan mendefinisikan fungsi dalam fungsi lainnya), menjalankan fungsi anggota kontainer atau meneruskan wadah sebagai argumen ke fungsi perpustakaan tidak akan membatalkan iterator untuk, atau mengubah nilai-nilai, objek dalam wadah itu. " ? Tapi vector.push_backJANGAN menentukan sebaliknya. "Menyebabkan realokasi jika ukuran baru lebih besar dari kapasitas yang lama." dan (at reserve) "Realokasi membatalkan semua referensi, petunjuk, dan iterator yang mengacu pada elemen dalam urutan."
Ben Voigt
13

Standar ini menjamin bahkan contoh pertama Anda menjadi aman. Mengutip C ++ 11

[sequence.reqmts]

3 Dalam Tabel 100 dan 101 ... Xmenunjukkan kelas wadah urutan, amenunjukkan nilai Xelemen yang mengandung tipe T, ... tmenunjukkan nilai lv atau nilai konstanta dariX::value_type

16 Tabel 101 ...

Ekspresi a.push_back(t) Jenis pengembalian void Semantik operasional Menambahkan salinan t. Membutuhkan: T akan CopyInsertablemenjadi X. Kontainer basic_string , deque, list,vector

Jadi, meskipun itu tidak sepele, implementasi harus menjamin itu tidak akan membatalkan referensi ketika melakukan push_back.

Angew tidak lagi bangga dengan SO
sumber
7
Saya tidak melihat bagaimana ini menjamin ini aman.
jrok
4
@ Angew: Ini benar-benar tidak valid t, satu-satunya pertanyaan adalah apakah sebelum atau setelah membuat salinan. Kalimat terakhir Anda tentu salah.
Ben Voigt
4
@BenVoigt Sejak tmemenuhi prasyarat yang tercantum, perilaku yang dijelaskan dijamin. Suatu implementasi tidak diizinkan untuk membatalkan prasyarat dan kemudian menggunakannya sebagai alasan untuk tidak berperilaku seperti yang ditentukan.
bames53
8
@ BenVoigt Klien tidak berkewajiban untuk mempertahankan prasyarat selama panggilan berlangsung; hanya untuk memastikan bahwa hal itu dipenuhi pada permulaan panggilan.
bames53
6
@ BenVoigt Itu poin yang bagus tapi saya percaya ada fungsi yang dilewatkan ke for_eachyang diperlukan untuk tidak membatalkan iterators. Saya tidak dapat menemukan referensi untuk for_each, tetapi saya melihat pada beberapa algoritma teks seperti "op dan binary_op tidak akan membatalkan iterators atau subranges".
bames53
7

Tidak jelas bahwa contoh pertama aman, karena implementasi paling sederhana push_backadalah dengan terlebih dahulu realokasi vektor, jika perlu, dan kemudian menyalin referensi.

Tetapi setidaknya tampaknya aman dengan Visual Studio 2010. Implementasinya push_backmelakukan penanganan khusus kasus ketika Anda menekan kembali suatu elemen dalam vektor. Kode ini disusun sebagai berikut:

void push_back(const _Ty& _Val)
    {   // insert element at end
    if (_Inside(_STD addressof(_Val)))
        {   // push back an element
                    ...
        }
    else
        {   // push back a non-element
                    ...
        }
    }
Johan Råde
sumber
8
Saya ingin tahu apakah spesifikasinya mengharuskan ini aman.
Nawaz
1
Menurut Standar itu tidak perlu aman. Namun dimungkinkan untuk mengimplementasikannya dengan cara yang aman.
Ben Voigt
2
@BenVoigt Aku akan mengatakan itu adalah diperlukan untuk menjadi aman (lihat jawaban saya).
Angew tidak lagi bangga dengan SO
2
@ BenVoigt Pada saat Anda lulus referensi, itu valid.
Angew tidak lagi bangga dengan SO
4
@ Angew: Itu tidak cukup. Anda harus memberikan referensi yang tetap berlaku selama durasi panggilan, dan yang ini tidak.
Ben Voigt
3

Ini bukan jaminan dari standar, tetapi sebagai titik data lain, v.push_back(v[0])aman untuk libc ++ LLVM .

libc ++std::vector::push_back panggilan __push_back_slow_pathketika perlu merealokasi memori:

void __push_back_slow_path(_Up& __x) {
  allocator_type& __a = this->__alloc();
  __split_buffer<value_type, allocator_type&> __v(__recommend(size() + 1), 
                                                  size(), 
                                                  __a);
  // Note that we construct a copy of __x before deallocating
  // the existing storage or moving existing elements.
  __alloc_traits::construct(__a, 
                            _VSTD::__to_raw_pointer(__v.__end_), 
                            _VSTD::forward<_Up>(__x));
  __v.__end_++;
  // Moving existing elements happens here:
  __swap_out_circular_buffer(__v);
  // When __v goes out of scope, __x will be invalid.
}
Nate Kohl
sumber
Salinan tidak hanya harus dibuat sebelum membatalkan alokasi penyimpanan yang ada, tetapi sebelum pindah dari elemen yang ada. Saya kira pemindahan elemen yang sudah ada dilakukan __swap_out_circular_buffer, dalam hal ini implementasi ini memang aman.
Ben Voigt
@ BenVoigt: poin bagus, dan Anda memang benar bahwa pemindahan terjadi di dalam __swap_out_circular_buffer. (Saya telah menambahkan beberapa komentar untuk diperhatikan.)
Nate Kohl
1

Versi pertama jelas TIDAK aman:

Operasi pada iterator yang diperoleh dengan memanggil wadah perpustakaan standar atau fungsi anggota string dapat mengakses wadah yang mendasarinya, tetapi tidak boleh memodifikasinya. [Catatan: Secara khusus, operasi kontainer yang membatalkan iterator bertentangan dengan operasi pada iterator yang terkait dengan kontainer itu. - catatan akhir]

dari bagian 17.6.5.9


Perhatikan bahwa ini adalah bagian tentang perlombaan data, yang biasanya dipikirkan orang bersamaan dengan threading ... tetapi definisi sebenarnya melibatkan hubungan "terjadi sebelum", dan saya tidak melihat adanya hubungan pemesanan antara beberapa efek samping push_backdi bermain di sini, yaitu pembatalan referensi tampaknya tidak didefinisikan sebagai diperintahkan sehubungan dengan menyalin-membangun elemen ekor baru.

Ben Voigt
sumber
1
Harus dipahami bahwa itu adalah catatan, bukan aturan, jadi itu menjelaskan konsekuensi dari aturan sebelumnya ... dan konsekuensinya identik untuk referensi.
Ben Voigt
5
Hasilnya v[0]bukan iterator, juga push_back()tidak mengambil iterator. Jadi, dari sudut pandang pengacara bahasa, argumen Anda tidak berlaku. Maaf. Saya tahu, bahwa sebagian besar iterator adalah pointer, dan titik pembatalan iterator, hampir sama dengan referensi, tetapi bagian dari standar yang Anda kutip, tidak relevan dengan situasi yang dihadapi.
cmaster - mengembalikan monica
-1. Ini adalah kutipan yang sama sekali tidak relevan dan toh tidak menjawabnya. Panitia mengatakan x.push_back(x[0])itu AMAN.
Nawaz
0

Benar-benar aman.

Dalam contoh kedua yang Anda miliki

v.reserve(v.size() + 1);

yang tidak diperlukan karena jika vektor keluar dari ukurannya, itu akan berarti reserve.

Vektor bertanggung jawab atas hal ini, bukan Anda.

Zaffy
sumber
-1

Keduanya aman karena push_back akan menyalin nilainya, bukan referensi. Jika Anda menyimpan pointer, itu masih aman sejauh menyangkut vektor, tetapi ketahuilah bahwa Anda akan memiliki dua elemen vektor yang menunjuk ke data yang sama.

Bagian 23.2.1 Persyaratan Wadah Umum

16
  • a.push_back (t) Menambahkan salinan t. Membutuhkan: T akan menjadi CopyInsertable ke X.
  • a.push_back (rv) Menambahkan salinan rv. Membutuhkan: T akan menjadi MoveInsertable ke X.

Implementasi push_back karena itu harus memastikan bahwa salinan v[0] dimasukkan. Dengan contoh balasan, dengan asumsi implementasi yang akan dialokasikan kembali sebelum menyalin, itu tidak akan dengan pasti menambahkan salinan v[0]dan dengan demikian melanggar spesifikasi.

OlivierD
sumber
2
push_backakan tetapi juga mengubah ukuran vektor, dan dalam implementasi yang naif ini akan membatalkan referensi sebelum menyalin terjadi. Jadi kecuali Anda dapat mendukungnya dengan kutipan dari standar, saya akan menganggapnya salah.
Konrad Rudolph
4
Dengan "ini", maksud Anda contoh pertama atau kedua? push_backakan menyalin nilai ke dalam vektor; tetapi (sejauh yang saya bisa lihat) yang mungkin terjadi setelah realokasi, pada titik mana referensi yang dicoba disalin tidak lagi valid.
Mike Seymour
1
push_backmenerima argumennya dengan referensi .
bames53
1
@OlivierD: Ini harus (1) mengalokasikan ruang baru (2) menyalin elemen baru (3) memindahkan-membangun elemen yang ada (4) menghancurkan elemen yang dipindahkan-dari (5) membebaskan penyimpanan lama - dalam urutan BAHWA - untuk membuat versi pertama berfungsi.
Ben Voigt
1
@BenVoigt mengapa lagi sebuah wadah mengharuskan jenis menjadi CopyInsertable jika itu akan benar-benar mengabaikan properti itu?
OlivierD
-2

Dari 23.3.6.5/1: Causes reallocation if the new size is greater than the old capacity. If no reallocation happens, all the iterators and references before the insertion point remain valid.

Karena kami menyisipkan di akhir, tidak ada referensi yang akan dibatalkan jika vektor tidak diubah ukurannya. Jadi jika vektor capacity() > size()maka dijamin akan berfungsi, jika tidak dijamin tidak akan terdefinisi.

Markus B
sumber
Saya percaya bahwa spesifikasi sebenarnya menjamin ini berfungsi dalam kedua kasus. Saya sedang menunggu referensi.
bames53
Tidak disebutkan tentang iterator atau keselamatan iterator dalam pertanyaan tersebut.
OlivierD
3
@OlivierD bagian iterator berlebihan di sini: Saya tertarik dengan referencesbagian dari kutipan.
Mark B
2
Ini sebenarnya dijamin aman (lihat jawaban saya, semantik push_back).
Angew tidak lagi bangga dengan SO