Può std :: vector emplace_back copiare il costrutto da un elemento del vettore stesso?

Quando si usa push_back di std::vector , posso spingere un elemento del vettore stesso senza timore di invalidare l’argomento dovuto alla riallocazione:

 std::vector v = { "a", "b" }; v.push_back(v[0]); // This is ok even if v.capacity() == 2 before this call. 

Tuttavia, quando si usa emplace_back , std::vector inoltra l’argomento al costruttore di std::string modo che la costruzione della copia avvenga in posizione nel vettore. Questo mi fa sospettare che la riallocazione del vettore avvenga prima che la nuova stringa sia costruita su una copia (altrimenti non sarebbe allocata sul posto), invalidando così l’argomento prima dell’uso.

Questo significa che non posso aggiungere un elemento del vettore stesso con emplace_back , o abbiamo qualche tipo di garanzia in caso di riallocazione, simile a push_back ?

Nel codice:

 std::vector v = { "a", "b" }; v.emplace_back(v[0]); // Is this valid, even if v.capacity() == 2 before this call? 

emplace_back deve essere sicuro per la stessa ragione per cui push_back deve essere sicuro ; l’invalidazione di puntatori e riferimenti ha effetto solo dopo il ritorno della chiamata al metodo di modifica.

In pratica, ciò significa che emplace_back che esegue una riallocazione è necessario per procedere nel seguente ordine (ignorando la gestione degli errori):

  1. Assegna nuova capacità
  2. Emplace-construct new element alla fine del nuovo segmento di dati
  3. Sposta: costruisci elementi esistenti nel nuovo segmento di dati
  4. Distruggi e deallocate il vecchio segmento di dati

In questo thread reddit STL riconosce un errore di VC11 per supportare v.emplace_back(v[0]) come un bug, quindi dovresti assolutamente verificare se la tua libreria supporta questo utilizzo e non v.emplace_back(v[0]) per scontato.

Si noti che alcune forms di auto-inserimento sono specificamente proibite dallo Standard; per esempio in [sequence.reqmts] paragrafo 4 Tabella 100 a.insert(p,i,j) ha il prerequisito ” i e j non sono iteratori in a “.

Contrariamente a quanto poche altre persone hanno scritto qui, ho fatto l’esperienza questa settimana che questo non è sicuro , almeno quando si cerca di avere un codice portatile con un comportamento definito.

Ecco alcuni esempi di codice che possono esporre un comportamento non definito:

 std::vector v; v.push_back(0); // some more push backs into v followed but are not shown here... v.emplace_back(v.back()); // problem is here! 

Il codice precedente funzionava su Linux con un STL g ++ senza problemi.

Quando si eseguiva lo stesso codice su Windows (compilato con Visual Studio 2013 Update5), il vettore a volte conteneva alcuni elementi alterati (valori apparentemente casuali).

Il motivo è che il riferimento restituito da v.back() stato invalidato a causa del raggiungimento del limite di capacità del contenitore all’interno di v.emplace_back() , prima che l’elemento fosse aggiunto alla fine.

Ho esaminato l’implementazione STL di emplace_back() ++ di emplace_back() e sembrava allocare nuovo spazio di archiviazione, copiare gli elementi vettoriali esistenti nella nuova posizione di archiviazione, liberare il vecchio spazio di archiviazione e quindi build l’elemento alla fine del nuovo archivio. A quel punto, la memoria sottostante dell’elemento di riferimento potrebbe essere già stata liberata o altrimenti invalidata. Stava producendo un comportamento indefinito, causando la confusione degli elementi vettoriali inseriti nelle soglie di riallocazione.

Questo sembra essere un bug (ancora non risolto) in Visual Studio . Con altre implementazioni STL che ho provato, il problema non si è verificato.

Alla fine, dovresti evitare di passare un riferimento ad un elemento vettoriale allo stesso vettore emplace_back() per ora, almeno se il tuo codice viene compilato con Visual Studio e dovrebbe funzionare.

Ho controllato la mia implementazione vettoriale e funziona qui come segue:

  1. Assegna nuova memoria
  2. Inserisci object
  3. Dealloc vecchia memoria

Quindi tutto va bene qui. Un’implementazione simile è utilizzata per push_back quindi questa va bene due.

Cordiali saluti, ecco la parte rilevante dell’attuazione. Ho aggiunto commenti:

 template template void vector<_Tp, _Alloc>:: _M_emplace_back_aux(_Args&&... __args) { const size_type __len = _M_check_len(size_type(1), "vector::_M_emplace_back_aux"); // HERE WE DO THE ALLOCATION pointer __new_start(this->_M_allocate(__len)); pointer __new_finish(__new_start); __try { // HERE WE EMPLACE THE ELEMENT _Alloc_traits::construct(this->_M_impl, __new_start + size(), std::forward<_args>(__args)...); __new_finish = 0; __new_finish = std::__uninitialized_move_if_noexcept_a (this->_M_impl._M_start, this->_M_impl._M_finish, __new_start, _M_get_Tp_allocator()); ++__new_finish; } __catch(...) { if (!__new_finish) _Alloc_traits::destroy(this->_M_impl, __new_start + size()); else std::_Destroy(__new_start, __new_finish, _M_get_Tp_allocator()); _M_deallocate(__new_start, __len); __throw_exception_again; } std::_Destroy(this->_M_impl._M_start, this->_M_impl._M_finish, _M_get_Tp_allocator()); // HERE WE DESTROY THE OLD MEMORY _M_deallocate(this->_M_impl._M_start, this->_M_impl._M_end_of_storage - this->_M_impl._M_start); this->_M_impl._M_start = __new_start; this->_M_impl._M_finish = __new_finish; this->_M_impl._M_end_of_storage = __new_start + __len; }