È sicuro respingere un elemento dallo stesso vettore?


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

Se il secondo push_back provoca una riallocazione, il riferimento al primo numero intero nel vettore non sarà più valido. Quindi questo non è sicuro?

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

Questo lo rende sicuro?


4
Nota: al momento c'è una discussione nel forum delle proposte standard. Come parte di esso, qualcuno ha fornito un esempio di implementazione dipush_back . Un altro poster ha notato un bug in esso , che non gestiva correttamente il caso che descrivi. Nessun altro, per quanto posso dire, ha sostenuto che non si trattava di un bug. Non dire che è una prova conclusiva, solo un'osservazione.
Benjamin Lindley,

9
Mi dispiace ma non so quale risposta accettare, in quanto vi sono ancora controversie sulla risposta corretta.
Neil Kirk,

4
Mi è stato chiesto di commentare questa domanda dal 5 ° commento sotto: stackoverflow.com/a/18647445/576911 . Lo sto facendo votando ogni risposta che dice attualmente: sì, è sicuro respingere un elemento dallo stesso vettore.
Howard Hinnant,

2
@BenVoigt: <shrug> Se non sei d'accordo con ciò che dice lo standard, o anche se sei d'accordo con lo standard, ma non pensi che lo dica abbastanza chiaramente, questa è sempre un'opzione per te: cplusplus.github.io/LWG/ lwg-active.html # submit_issue Ho preso questa opzione me stesso più volte di quanto possa ricordare. A volte con successo, a volte no. Se vuoi discutere di ciò che dice lo standard o di cosa dovrebbe dire, SO non è un forum efficace. La nostra conversazione non ha significato normativo. Ma puoi avere la possibilità di avere un impatto normativo seguendo il link sopra.
Howard Hinnant,

2
@ Polaris878 Se push_back fa sì che il vettore raggiunga la sua capacità, il vettore assegnerà un nuovo buffer più grande, copierà i vecchi dati e quindi eliminerà il vecchio buffer. Quindi inserirà il nuovo elemento. Il problema è che il nuovo elemento è un riferimento ai dati nel vecchio buffer che è stato appena eliminato. A meno che push_back non faccia una copia del valore prima di eliminarlo, sarà un riferimento errato.
Neil Kirk,

Risposte:


31

Sembra che http://www.open-std.org/jtc1/sc22/wg21/docs/lwg-closed.html#526 abbia affrontato questo problema (o qualcosa di molto simile ad esso) come un potenziale difetto dello standard:

1) I parametri presi dal riferimento const possono essere modificati durante l'esecuzione della funzione

Esempi:

Dato std :: vector v:

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

v [2] può essere modificato spostando elementi del vettore

La risoluzione proposta era che questo non era un difetto:

vector :: insert (iter, value) è necessario per funzionare perché lo standard non autorizza il suo funzionamento.


Trovo l'autorizzazione in 17.6.4.9: "Se un argomento di una funzione ha un valore non valido (come un valore esterno al dominio della funzione o un puntatore non valido per l'uso previsto), il comportamento non è definito." Se si verifica la riallocazione, tutti gli iteratori e i riferimenti agli elementi vengono invalidati, il che significa che anche il riferimento al parametro passato alla funzione non è valido.
Ben Voigt,

4
Penso che il punto sia che l'implementazione è responsabile per la riallocazione. Spetta a esso assicurarsi che il comportamento sia definito se l'input è inizialmente definito. Poiché le specifiche specificano chiaramente che push_back esegue una copia, le implementazioni devono, a scapito del tempo di esecuzione, memorizzare nella cache o copiare tutti i valori prima di disallocare. Poiché in questa particolare domanda non sono rimasti riferimenti esterni, non importa se iteratori e riferimenti sono invalidati.
OlivierD,

3
@NeilKirk Penso che questa dovrebbe essere la risposta autorevole, è anche menzionata da Stephan T. Lavavej su Reddit usando essenzialmente gli stessi argomenti.
TemplateRex,

v.insert(v.begin(), v[2]);impossibile attivare una riallocazione. Quindi, come risponde alla domanda?
ThomasMcLeod

@ThomasMcLeod: sì, ovviamente può innescare una riallocazione. Stai espandendo la dimensione del vettore inserendo un nuovo elemento.
Violet Giraffe,

21

Sì, è sicuro e le implementazioni di librerie standard saltano attraverso i cerchi per renderlo tale.

Credo che gli implementatori riportino questo requisito al 23.2 / 11 in qualche modo, ma non riesco a capire come, e non riesco a trovare qualcosa di più concreto. Il meglio che posso trovare è questo articolo:

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

Il controllo delle implementazioni di libc ++ e libstdc ++ mostra che sono anche sicuri.


9
Qualche supporto sarebbe davvero d'aiuto qui.
chris,

3
Questo è interessante, devo ammettere che non avevo mai considerato il caso, ma in effetti sembra abbastanza difficile da raggiungere. Vale anche per vec.insert(vec.end(), vec.begin(), vec.end());?
Matthieu M.,

2
@MatthieuM. No: la tabella 100 dice: "pre: iej non sono iteratori in un".
Sebastian Redl,

2
Adesso sto votando perché anche questo è il mio ricordo, ma è necessario un riferimento.
bames53,

3
È 23.2 / 11 nella versione che stai utilizzando "Salvo diversamente specificato (esplicitamente o definendo una funzione in termini di altre funzioni), invocare una funzione membro del contenitore o passare un contenitore come argomento a una funzione di libreria non invalida gli iteratori o modificare i valori di oggetti all'interno di quel contenitore. " ? Ma vector.push_backaltrimenti specifica. "Provoca la riallocazione se la nuova dimensione è maggiore della capacità precedente." e (at reserve) "La riallocazione invalida tutti i riferimenti, i puntatori e gli iteratori che si riferiscono agli elementi nella sequenza."
Ben Voigt,

13

Lo standard garantisce che anche il tuo primo esempio sia sicuro. Citando C ++ 11

[sequence.reqmts]

3 Nelle tabelle 100 e 101 ... Xindica una classe contenitore sequenza, aindica un valore Xcontenente elementi di tipo T, ... tindica un valore o un valore costante diX::value_type

16 Tabella 101 ...

Espressione a.push_back(t) Tipo di ritorno void Semantica operativa Aggiunge una copia di t. Richiede: T deve essere CopyInsertablein X. Contenitore basic_string , deque, list,vector

Quindi, anche se non è esattamente banale, l'implementazione deve garantire che non invaliderà il riferimento durante l'esecuzione di push_back.


7
Non vedo come questo garantisca che ciò sia sicuro.
jrok,

4
@Angew: invalida assolutamente t, l'unica domanda è se prima o dopo aver effettuato la copia. La tua ultima frase è sicuramente sbagliata.
Ben Voigt,

4
@BenVoigt Poiché tsoddisfa i requisiti elencati, il comportamento descritto è garantito. Un'implementazione non può invalidare una condizione preliminare e quindi utilizzarla come scusa per non comportarsi come specificato.
bames53,

8
@BenVoigt Il cliente non è obbligato a mantenere il presupposto per tutta la chiamata; solo per garantire che sia soddisfatto all'inizio della chiamata.
bames53,

6
@BenVoigt Questo è un buon punto, ma credo che ci for_eachsia bisogno che il funzione passato a non invalidi gli iteratori. Non riesco a trovare un riferimento per for_each, ma vedo su alcuni algoritmi testi come "op e binary_op non invalideranno iteratori o subrange".
bames53,

7

Non è ovvio che il primo esempio sia sicuro, poiché l'implementazione più semplice di push_backsarebbe innanzitutto riallocare il vettore, se necessario, e quindi copiare il riferimento.

Ma almeno sembra essere sicuro con Visual Studio 2010. La sua implementazione push_backfa una gestione speciale del caso quando si respinge un elemento nel vettore. Il codice è strutturato come segue:

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

8
Vorrei sapere se le specifiche richiedono che ciò sia sicuro.
Nawaz,

1
Secondo lo standard non è necessario per essere sicuro. È possibile, tuttavia, implementarlo in modo sicuro.
Ben Voigt,

2
@BenVoigt Direi che è necessario essere al sicuro (vedi la mia risposta).
Angew non è più orgoglioso di SO

2
@BenVoigt Al momento in cui si passa il riferimento, è valido.
Angew non è più orgoglioso di SO

4
@Angew: non è sufficiente. Devi passare un riferimento che rimanga valido per la durata della chiamata e questo no.
Ben Voigt,

3

Questa non è una garanzia dallo standard, ma come un altro punto dati, v.push_back(v[0])è sicura per libc ++ di LLVM .

Lestd::vector::push_back chiamate di libc ++__push_back_slow_path quando è necessario riallocare la memoria:

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

La copia deve essere effettuata non solo prima di deallocare la memoria esistente, ma prima di spostarsi dagli elementi esistenti. Suppongo che lo spostamento di elementi esistenti sia stato effettuato __swap_out_circular_buffer, nel qual caso questa implementazione è davvero sicura.
Ben Voigt,

@BenVoigt: buon punto, e in effetti hai ragione che il movimento avviene all'interno __swap_out_circular_buffer. (Ho aggiunto alcuni commenti per notare che.)
Nate Kohl,

1

La prima versione NON è assolutamente sicura:

Le operazioni su iteratori ottenute chiamando un contenitore di libreria standard o una funzione membro stringa possono accedere al contenitore sottostante, ma non devono modificarlo. [Nota: in particolare, le operazioni del contenitore che invalidano gli iteratori sono in conflitto con le operazioni sugli iteratori associati a quel contenitore. - nota finale]

dalla sezione 17.6.5.9


Si noti che questa è la sezione sulle corse di dati, a cui la gente normalmente pensa in congiunzione con il threading ... ma la definizione effettiva implica relazioni "prima" e non vedo alcuna relazione di ordinamento tra i molteplici effetti collaterali di push_backin gioca qui, vale a dire che l'invalidazione di riferimento sembra non essere definita come ordinata rispetto alla copia-costruzione del nuovo elemento di coda.


1
Dovrebbe essere compreso che è una nota, non una regola, quindi sta spiegando una conseguenza della regola precedente ... e le conseguenze sono identiche per i riferimenti.
Ben Voigt,

5
Il risultato di v[0]non è un iteratore, allo stesso modo, push_back()non prende un iteratore. Quindi, dal punto di vista dell'avvocato linguista, la tua argomentazione è nulla. Scusate. So che la maggior parte degli iteratori sono puntatori e che il punto di invalidare un iteratore è praticamente lo stesso dei riferimenti, ma la parte dello standard che citi è irrilevante per la situazione attuale.
cmaster - reinstalla monica il

-1. È una citazione completamente irrilevante e non risponde comunque. Il comitato afferma che x.push_back(x[0])è SICURO.
Nawaz,

0

È completamente sicuro.

Nel tuo secondo esempio hai

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

che non è necessario perché se il vettore esce dalle sue dimensioni, implica il reserve.

Vector è responsabile di questa roba, non tu.


-1

Entrambi sono sicuri poiché push_back copierà il valore, non il riferimento. Se stai memorizzando puntatori, questo è ancora sicuro per quanto riguarda il vettore, ma sappi solo che avrai due elementi del tuo vettore che puntano agli stessi dati.

Sezione 23.2.1 Requisiti generali del contenitore

16
  • a.push_back (t) Aggiunge una copia di t. Richiede: T deve essere CopyInsertable in X.
  • a.push_back (rv) Aggiunge una copia di rv. Richiede: T deve essere MoveInsertable in X.

Le implementazioni di push_back devono pertanto garantire l'inserimento di una copia di v[0] . Per contro esempio, supponendo un'implementazione che si riallocerebbe prima della copia, non aggiungerebbe sicuramente una copia v[0]e in quanto tale violerebbe le specifiche.


2
push_backridimensionerà comunque anche il vettore e in un'implementazione ingenua ciò invaliderà il riferimento prima che si verifichi la copia. Quindi, a meno che non sia possibile sostenerlo con una citazione dallo standard, lo considererò sbagliato.
Konrad Rudolph,

4
Con "questo" intendi il primo o il secondo esempio? push_backcopierà il valore nel vettore; ma (per quanto posso vedere) ciò potrebbe accadere dopo la riallocazione, a quel punto il riferimento da cui sta tentando di copiare non è più valido.
Mike Seymour,

1
push_backriceve il suo argomento per riferimento .
bames53,

1
@OlivierD: Dovrebbe (1) allocare nuovo spazio (2) copiare il nuovo elemento (3) spostare-costruire gli elementi esistenti (4) distruggere gli elementi spostati (5) liberare la vecchia memoria - in QUESTO ordine - per far funzionare la prima versione.
Ben Voigt,

1
@BenVoigt perché altrimenti un contenitore richiederebbe che un tipo sia CopyInsertable se comunque ignorerà completamente quella proprietà?
OlivierD,

-2

Dal 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.

Poiché stiamo inserendo alla fine, nessun riferimento verrà invalidato se il vettore non viene ridimensionato. Quindi, se il vettore è capacity() > size()quindi garantito, altrimenti è garantito un comportamento indefinito.


Credo che la specifica garantisca effettivamente che funzioni in entrambi i casi. Sto aspettando un riferimento però.
bames53,

Non vi è alcuna menzione degli iteratori o della sicurezza dell'iteratore nella domanda.
OlivierD,

3
@OlivierD la parte iteratore è superflua qui: mi interessa la referencesparte della citazione.
Marco B,

2
In realtà è garantito per essere sicuro (vedi la mia risposta, semantica di push_back).
Angew non è più orgoglioso di SO
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.