É seguro enviar push_back a um elemento do mesmo vetor?

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

Se o segundo push_back causar uma realocação, a referência ao primeiro número inteiro no vetor não será mais válida. Então isso não é seguro?

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

Isso torna seguro?

Neil Kirk
fonte
4
Uma observação: Atualmente, há uma discussão no fórum de propostas padrão. Como parte disso, alguém deu um exemplo de implementação depush_back . Outro pôster observou um bug , que não tratava adequadamente o caso que você descreve. Ninguém mais, pelo que sei, argumentou que isso não era um bug. Não estou dizendo que é uma prova conclusiva, apenas uma observação.
Benjamin Lindley
9
Sinto muito, mas não sei qual resposta aceitar, pois ainda há controvérsia sobre a resposta correta.
22413 Neil Kirk
4
Fui convidado a comentar esta pergunta pelo quinto comentário em: stackoverflow.com/a/18647445/576911 . Estou fazendo isso revogando todas as respostas que atualmente dizem: sim, é seguro fazer pushback de um elemento do mesmo vetor.
Howard Hinnant
2
@BenVoigt: <shrug> Se você não concorda com o que o padrão diz, ou mesmo se concorda com o padrão, mas não o acha claramente, isso sempre é uma opção para você: cplusplus.github.io/LWG/ lwg-active.html # submit_issue Fiz essa opção mais vezes do que me lembro. Às vezes com sucesso, às vezes não. Se você deseja debater o que o padrão diz ou o que deveria dizer, o SO não é um fórum eficaz. Nossa conversa não tem significado normativo. Mas você pode ter um impacto normativo seguindo o link acima.
Howard Hinnant 15/09/13
2
@ Polaris878 Se o push_back fizer com que o vetor atinja sua capacidade, o vetor alocará um novo buffer maior, copiará os dados antigos e excluirá o buffer antigo. Em seguida, ele inserirá o novo elemento. O problema é que o novo elemento é uma referência aos dados no buffer antigo que acaba de ser excluído. A menos que push_back faça uma cópia do valor antes de excluir, será uma má referência.
Neil Kirk

Respostas:

31

Parece que http://www.open-std.org/jtc1/sc22/wg21/docs/lwg-closed.html#526 abordou esse problema (ou algo muito semelhante a ele) como um potencial defeito no padrão:

1) Os parâmetros tomados pela referência const podem ser alterados durante a execução da função

Exemplos:

Dado std :: vector v:

inserção de v (v.begin (), v [2]);

v [2] pode ser alterado movendo elementos do vetor

A resolução proposta era que este não era um defeito:

É necessário que o vetor :: insert (iter, value) funcione porque o padrão não permite que ele não funcione.

Nate Kohl
fonte
Encontro permissão em 17.6.4.9: "Se um argumento para uma função tiver um valor inválido (como um valor fora do domínio da função ou um ponteiro inválido para o uso pretendido), o comportamento será indefinido." Se a realocação ocorrer, todos os iteradores e referências a elementos serão invalidados, significando que a referência de parâmetro passada para a função também será inválida.
Ben Voigt
4
Eu acho que o ponto é que a implementação é responsável por fazer a realocação. Cabe a ele garantir que o comportamento seja definido se a entrada for definida inicialmente. Como as especificações especificam claramente que o push_back faz uma cópia, as implementações devem, às custas do tempo de execução, armazenar em cache ou copiar todos os valores antes de desalocar. Como nesta pergunta em particular não há referências externas, não importa se iteradores e referências são invalidados.
precisa saber é o seguinte
3
@ NeilKirk Acho que essa deve ser a resposta oficial, também é mencionada por Stephan T. Lavavej no Reddit usando essencialmente os mesmos argumentos.
TemplateRex
v.insert(v.begin(), v[2]);não pode disparar uma realocação. Então, como isso responde à pergunta?
21417 Thomas ThompsonMcLeod 3/17
@ThomasMcLeod: sim, obviamente isso pode desencadear uma realocação. Você está expandindo o tamanho do vetor inserindo um novo elemento.
Violet Giraffe
21

Sim, é seguro e as implementações de bibliotecas padrão saltam através de argolas para fazê-lo.

Acredito que os implementadores rastreiam esse requisito de volta a 23.2 / 11 de alguma forma, mas não consigo descobrir como e também não consigo encontrar algo mais concreto. O melhor que posso encontrar é este artigo:

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

A inspeção das implementações do libc ++ e libstdc ++ mostra que elas também são seguras.

Sebastian Redl
fonte
9
Algum apoio realmente ajudaria aqui.
Chris
3
Isso é interessante, devo admitir que nunca havia considerado o caso, mas, na verdade, parece bastante difícil de alcançar. Também vale para vec.insert(vec.end(), vec.begin(), vec.end());?
Matthieu M.
2
@MatthieuM. Não: A Tabela 100 diz: "pre: iej não são iteradores em um".
Sebastian Redl 13/09/13
2
Estou votando agora, pois essa também é minha lembrança, mas é necessária uma referência.
precisa saber é o seguinte
3
É 23.2 / 11 na versão que você está usando "A menos que especificado de outra forma (explicitamente ou definindo uma função em termos de outras funções), invocar uma função de membro de contêiner ou passar um contêiner como argumento para uma função de biblioteca não invalidará os iteradores para alterar ou alterar os valores de objetos nesse contêiner ". ? Mas, vector.push_backcaso contrário, especifica. "Causa a realocação se o novo tamanho for maior que a capacidade antiga." e (at reserve) "A realocação invalida todas as referências, ponteiros e iteradores que se referem aos elementos na sequência."
Ben Voigt
13

O padrão garante que mesmo o seu primeiro exemplo seja seguro. Citando C ++ 11

[sequence.reqmts]

3 Nas tabelas 100 e 101 ... Xdenota uma classe de contêiner de sequência, adenota um valor de Xelementos do tipo T, ... tdenota um valor l ou um valor constante deX::value_type

16 Tabela 101 ...

Expressão a.push_back(t) Tipo de retorno void Semântica operacional Anexa uma cópia de t. Requer: T deve estar CopyInsertabledentro X. Container basic_string , deque, list,vector

Portanto, mesmo que não seja exatamente trivial, a implementação deve garantir que não invalidará a referência ao fazer o push_back.

Angew não se orgulha mais de SO
fonte
7
Não vejo como isso garante que isso seja seguro.
JROK
4
@ Angular: absolutamente invalida t, a única questão é se antes ou depois de fazer a cópia. Sua última frase está certamente errada.
Ben Voigt
4
@BenVoigt Desde que tatenda às pré-condições listadas, o comportamento descrito é garantido. Uma implementação não tem permissão para invalidar uma pré-condição e, em seguida, use isso como uma desculpa para não se comportar conforme especificado.
precisa saber é o seguinte
8
@BenVoigt O cliente não é obrigado a manter a pré-condição durante toda a chamada; apenas para garantir que ele seja atendido no início da chamada.
precisa saber é o seguinte
6
@BenVoigt Esse é um bom argumento, mas acredito que exista que o functor passado for_eachseja necessário para não invalidar os iteradores. Não consigo criar uma referência para for_each, mas vejo em alguns algoritmos o texto como "op e binary_op não invalidará iteradores ou subintervalos".
precisa saber é o seguinte
7

Não é óbvio que o primeiro exemplo é seguro, porque a implementação mais simples push_backseria realocar primeiro o vetor, se necessário, e depois copiar a referência.

Mas pelo menos parece seguro com o Visual Studio 2010. Sua implementação push_backfaz um tratamento especial do caso quando você empurra um elemento no vetor. O código está estruturado da seguinte maneira:

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
fonte
8
Gostaria de saber se a especificação exige que isso seja seguro.
Nawaz
1
De acordo com a Norma, não é necessário que seja seguro. É possível, no entanto, implementá-lo de maneira segura.
Ben Voigt
2
@BenVoigt, eu diria que é necessário ser seguro (veja minha resposta).
Angew não está mais orgulhoso de SO
2
@BenVoigt Quando você passa a referência, ela é válida.
Angew não está mais orgulhoso de SO
4
@ Angle: Isso não é suficiente. Você precisa passar por uma referência que permaneça válida durante a chamada e essa não.
Ben Voigt
3

Isso não é uma garantia do padrão, mas como outro ponto de dados, v.push_back(v[0])é seguro para o libc ++ do LLVM .

std::vector::push_backchamadas do libc ++__push_back_slow_path quando ele precisa realocar a memória:

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
fonte
A cópia não só deve ser feita antes da desalocação do armazenamento existente, mas antes de sair dos elementos existentes. Suponho que a movimentação dos elementos existentes seja feita __swap_out_circular_buffer, caso em que essa implementação é realmente segura.
Ben Voigt
@ BenVoigt: bom ponto, e você está realmente correto de que a mudança acontece dentro __swap_out_circular_buffer. (Eu adicionei alguns comentários notar que.)
Nate Kohl
1

A primeira versão definitivamente NÃO é segura:

Operações em iteradores obtidos chamando um contêiner de biblioteca padrão ou uma função de membro de string podem acessar o contêiner subjacente, mas não devem modificá-lo. [Nota: Em particular, operações de contêiner que invalidam iteradores entram em conflito com operações em iteradores associados a esse contêiner. - nota final]

da seção 17.6.5.9


Note-se que esta é a seção em corridas de dados, que as pessoas normalmente pensam em conjunto com enfiar ... mas a própria definição envolve "acontece antes" relacionamentos, e eu não vejo qualquer relação ordenação entre os vários efeitos colaterais de push_backem jogar aqui, ou seja, a invalidação de referência parece não ser definida como ordenada em relação à cópia-construção do novo elemento de cauda.

Ben Voigt
fonte
1
Deve-se entender que é uma nota, não uma regra, por isso está explicando uma consequência da regra anterior ... e as consequências são idênticas para referências.
Ben Voigt
5
O resultado de v[0]não é um iterador, da mesma forma, push_back()não leva um iterador. Portanto, do ponto de vista do advogado de idiomas, seu argumento é nulo. Desculpe. Eu sei que a maioria dos iteradores são indicadores, e o ponto de invalidar um iterador é praticamente o mesmo que para as referências, mas a parte do padrão que você cita é irrelevante para a situação em questão.
cmaster - reinstate monica
-1. É uma citação completamente irrelevante e não responde de qualquer maneira. O comitê diz que x.push_back(x[0])é SEGURO.
Nawaz
0

É completamente seguro.

No seu segundo exemplo você tem

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

o que não é necessário porque se o vetor sair do seu tamanho, isso implicará em reserve.

Vector é responsável por essas coisas, não você.

Zaffy
fonte
-1

Ambos são seguros, pois o push_back copiará o valor, não a referência. Se você estiver armazenando ponteiros, isso ainda é seguro no que diz respeito ao vetor, mas saiba que você terá dois elementos do vetor apontando para os mesmos dados.

Seção 23.2.1 Requisitos gerais de contêiner

16
  • a.push_back (t) Anexa uma cópia de t. Requer: T deve ser CopyInsertable em X.
  • a.push_back (rv) Anexa uma cópia do rv. Requer: T deve ser MoveInsertable em X.

As implementações de push_back devem, portanto, garantir que uma cópia de v[0] seja inserida. Por exemplo, assumindo que uma implementação seria realocada antes da cópia, ela certamente não acrescentaria uma cópia v[0]e, como tal, violaria as especificações.

OlivierD
fonte
2
push_backno entanto, também redimensionará o vetor e, em uma implementação ingênua, isso invalidará a referência antes que a cópia ocorra. Portanto, a menos que você possa fazer o backup com uma citação do padrão, considerarei errado.
21813 Konrad Rudolph
4
Com "this", você quer dizer o primeiro ou o segundo exemplo? push_backcopiará o valor no vetor; mas (tanto quanto posso ver) que pode acontecer após a realocação, quando a referência da qual está tentando copiar não é mais válida.
9139 Mike Seymour
1
push_backrecebe seu argumento por referência .
bames53
1
@OlivierD: Teria que (1) alocar novo espaço (2) copiar o novo elemento (3) mover-construir os elementos existentes (4) destruir os elementos movidos-de (5) liberar o armazenamento antigo - nessa ordem - para fazer a primeira versão funcionar.
Ben Voigt
1
@BenVoigt, por que mais um contêiner exigiria que um tipo fosse CopyInsertable se, de qualquer maneira, ignoraria completamente essa propriedade?
precisa saber é o seguinte
-2

A partir de 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.

Como estamos inserindo no final, nenhuma referência será invalidada se o vetor não for redimensionado. Portanto, se o vetor capacity() > size()for garantido, ele funcionará, caso contrário, será um comportamento indefinido.

Mark B
fonte
Acredito que a especificação realmente garanta que isso funcione nos dois casos. Estou esperando uma referência embora.
precisa saber é o seguinte
Não há menção de iteradores ou segurança do iterador na pergunta.
OlivierD
3
@ OlivierD a parte do iterador é supérflua aqui: estou interessado na referencesparte da citação.
Mark B
2
Na verdade, é garantido que ele é seguro (veja minha resposta, semântica de push_back).
Angew não está mais orgulhoso de SO