Est-il sûr de repousser un élément du même vecteur?

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

Si le second push_back provoque une réallocation, la référence au premier entier du vecteur ne sera plus valide. Donc ce n'est pas sûr?

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

Cela le rend sûr?

Neil Kirk
la source
4
Remarque: il y a actuellement une discussion dans le forum des propositions standard. Dans ce cadre, quelqu'un a donné un exemple de mise en œuvre depush_back . Une autre affiche a noté un bogue , qu'il ne traitait pas correctement le cas que vous décrivez. Personne d'autre, pour autant que je sache, n'a soutenu que ce n'était pas un bug. Ne pas dire que c'est une preuve concluante, juste une observation.
Benjamin Lindley
9
Je suis désolé mais je ne sais pas quelle réponse accepter car il y a encore une controverse sur la bonne réponse.
Neil Kirk
4
J'ai été invité à commenter cette question par le 5ème commentaire sous: stackoverflow.com/a/18647445/576911 . Je le fais en votant pour chaque réponse qui dit actuellement: oui, il est sûr de repousser un élément du même vecteur.
Howard Hinnant le
2
@BenVoigt: <shrug> Si vous n'êtes pas d'accord avec ce que dit la norme, ou même si vous êtes d'accord avec la norme, mais ne pensez pas qu'elle le dit assez clairement, c'est toujours une option pour vous: cplusplus.github.io/LWG/ lwg-active.html # submit_issue J'ai moi-même choisi cette option plus de fois que je ne m'en souviens. Parfois avec succès, parfois non. Si vous voulez débattre de ce que dit la norme, ou de ce qu'elle devrait dire, SO n'est pas un forum efficace. Notre conversation n'a pas de sens normatif. Mais vous pouvez avoir une chance d'avoir un impact normatif en suivant le lien ci-dessus.
Howard Hinnant
2
@ Polaris878 Si push_back amène le vecteur à atteindre sa capacité, le vecteur allouera un nouveau tampon plus grand, copiera les anciennes données, puis supprimera l'ancien tampon. Ensuite, il insérera le nouvel élément. Le problème est que le nouvel élément est une référence aux données de l'ancien tampon qui vient d'être supprimé. À moins que push_back fasse une copie de la valeur avant de la supprimer, ce sera une mauvaise référence.
Neil Kirk

Réponses:

31

Il semble que http://www.open-std.org/jtc1/sc22/wg21/docs/lwg-closed.html#526 a abordé ce problème (ou quelque chose de très similaire) comme un défaut potentiel de la norme:

1) Les paramètres pris par référence const peuvent être modifiés lors de l'exécution de la fonction

Exemples:

Étant donné std :: vector v:

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

v [2] peut être modifié en déplaçant les éléments du vecteur

La résolution proposée était qu'il ne s'agissait pas d'un défaut:

vector :: insert (iter, value) est requis pour fonctionner car le standard ne donne pas la permission pour qu'il ne fonctionne pas.

Nate Kohl
la source
Je trouve la permission dans 17.6.4.9: "Si un argument à une fonction a une valeur invalide (comme une valeur en dehors du domaine de la fonction ou un pointeur invalide pour son utilisation prévue), le comportement n'est pas défini." En cas de réallocation, tous les itérateurs et références aux éléments sont invalides, ce qui signifie que la référence de paramètre transmise à la fonction est également invalide.
Ben Voigt
4
Je pense que le fait est que la mise en œuvre est responsable de la réaffectation. Il lui incombe de s'assurer que le comportement est défini si l'entrée est définie initialement. Puisque les spécifications spécifient clairement que push_back effectue une copie, les implémentations doivent, au détriment du temps d'exécution peut-être, mettre en cache ou copier toutes les valeurs avant de désallouer. Puisque dans cette question particulière, il n'y a plus de références externes, peu importe si les itérateurs et les références sont invalidés.
OlivierD
3
@NeilKirk Je pense que cela devrait être la réponse autoritaire, elle est également mentionnée par Stephan T. Lavavej sur Reddit en utilisant essentiellement les mêmes arguments.
TemplateRex
v.insert(v.begin(), v[2]);ne peut pas déclencher une réallocation. Alors, comment cela répond-il à la question?
ThomasMcLeod
@ThomasMcLeod: oui cela peut évidemment déclencher une réallocation. Vous augmentez la taille du vecteur en insérant un nouvel élément.
Violet Giraffe
21

Oui, c'est sûr, et les implémentations de bibliothèques standard sautent à travers les obstacles pour y parvenir.

Je pense que les développeurs font remonter cette exigence à 23.2 / 11 d'une manière ou d'une autre, mais je ne peux pas comprendre comment, et je ne peux pas trouver quelque chose de plus concret non plus. Le mieux que je puisse trouver est cet article:

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

L'inspection des implémentations de libc ++ et libstdc ++ montre qu'elles sont également sûres.

Sebastian Redl
la source
9
Un soutien serait vraiment utile ici.
chris
3
C'est intéressant, je dois avouer que je n'avais jamais envisagé le cas mais en effet cela semble assez difficile à réaliser. Cela vaut-il aussi pour vec.insert(vec.end(), vec.begin(), vec.end());?
Matthieu M.
2
@MatthieuM. Non: le tableau 100 dit: "pre: i et j ne sont pas des itérateurs dans a".
Sebastian Redl
2
Je vote pour l'instant car c'est aussi mon souvenir, mais une référence est nécessaire.
bames53
3
Est 23.2 / 11 dans la version que vous utilisez "Sauf indication contraire (soit explicitement, soit en définissant une fonction en termes d'autres fonctions), invoquer une fonction membre de conteneur ou passer un conteneur comme argument à une fonction de bibliothèque n'invalidera pas les itérateurs à, ou modifier les valeurs des objets dans ce conteneur. " ? Mais vector.push_backspécifie le contraire. "Provoque une réallocation si la nouvelle taille est supérieure à l'ancienne capacité." et (at reserve) "La réallocation invalide toutes les références, pointeurs et itérateurs faisant référence aux éléments de la séquence."
Ben Voigt
13

La norme garantit même votre premier exemple d'être en sécurité. Citant C ++ 11

[sequence.reqmts]

3 Dans les tableaux 100 et 101 ... Xdésigne une classe de conteneur de séquence, adésigne une valeur d' Xéléments contenant de type T, ... tdésigne une lvalue ou une const rvalue deX::value_type

16 Tableau 101 ...

Expression a.push_back(t) Type de retour void Sémantique opérationnelle Ajoute une copie de t. Requiert: T doit être CopyInsertabledans X. Conteneur basic_string , deque, list,vector

Ainsi, même si ce n'est pas vraiment trivial, l'implémentation doit garantir qu'elle n'invalidera pas la référence lors de l'exécution du push_back.

Angew n'est plus fier de SO
la source
7
Je ne vois pas comment cela garantit la sécurité.
jrok
4
@Angew: Cela invalide absolument t, la seule question est de savoir si avant ou après la copie. Votre dernière phrase est certainement fausse.
Ben Voigt
4
@BenVoigt Puisque tremplit les conditions préalables énumérées, le comportement décrit est garanti. Une implémentation n'est pas autorisée à invalider une condition préalable, puis à l'utiliser comme excuse pour ne pas se comporter comme spécifié.
bames53
8
@BenVoigt Le client n'est pas obligé de maintenir la condition préalable tout au long de l'appel; uniquement pour s'assurer qu'il est satisfait au lancement de l'appel.
bames53
6
@BenVoigt C'est un bon point mais je crois qu'il y a que le foncteur passé à for_eachest nécessaire pour ne pas invalider les itérateurs. Je ne peux pas trouver de référence pour for_each, mais je vois sur certains algorithmes du texte comme "op et binary_op n'invalideront pas les itérateurs ou les sous-plages".
bames53
7

Il n'est pas évident que le premier exemple soit sûr, car l'implémentation la plus simple de push_backserait d'abord de réallouer le vecteur, si nécessaire, puis de copier la référence.

Mais au moins, cela semble être sûr avec Visual Studio 2010. Son implémentation de push_backfait une gestion spéciale du cas lorsque vous repoussez un élément dans le vecteur. Le code est structuré comme suit:

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
la source
8
Je voudrais savoir si la spécification l'exige pour être sûr.
Nawaz
1
Selon la norme, il n'est pas nécessaire d'être sûr. Il est cependant possible de le mettre en œuvre de manière sûre.
Ben Voigt
2
@BenVoigt Je dirais qu'il est nécessaire d'être en sécurité (voir ma réponse).
Angew n'est plus fier de SO
2
@BenVoigt Au moment où vous passez la référence, elle est valide.
Angew n'est plus fier de SO
4
@Angew: Ce n'est pas suffisant. Vous devez transmettre une référence qui reste valide pendant toute la durée de l'appel, et celle-ci ne l'est pas.
Ben Voigt
3

Ce n'est pas une garantie de la norme, mais comme un autre point de données, il v.push_back(v[0])est sans danger pour la libc ++ de LLVM .

std::vector::push_backAppels de libc ++__push_back_slow_path quand il a besoin de réallouer de la mémoire:

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
la source
La copie doit non seulement être effectuée avant de désallouer le stockage existant, mais avant de quitter les éléments existants. Je suppose que le déplacement des éléments existants se fait dans __swap_out_circular_buffer, auquel cas cette implémentation est en effet sûre.
Ben Voigt
@BenVoigt: bon point, et vous avez en effet raison que le déplacement se passe à l'intérieur __swap_out_circular_buffer. (J'ai ajouté quelques commentaires pour le noter.)
Nate Kohl
1

La première version n'est certainement PAS sûre:

Les opérations sur les itérateurs obtenues en appelant un conteneur de bibliothèque standard ou une fonction membre de chaîne peuvent accéder au conteneur sous-jacent, mais ne doivent pas le modifier. [Remarque: en particulier, les opérations de conteneur qui invalident les itérateurs sont en conflit avec les opérations sur les itérateurs associés à ce conteneur. - note de fin]

de la section 17.6.5.9


Notez qu'il s'agit de la section sur les courses de données, à laquelle les gens pensent normalement en conjonction avec le threading ... mais la définition réelle implique des relations «se produit avant», et je ne vois aucune relation d'ordre entre les multiples effets secondaires de push_backin play ici, à savoir que l'invalidation de référence ne semble pas être définie comme ordonnée par rapport à la construction par copie du nouvel élément de queue.

Ben Voigt
la source
1
Il faut comprendre que c'est une note, pas une règle, donc cela explique une conséquence de la règle précédente ... et les conséquences sont identiques pour les références.
Ben Voigt
5
Le résultat de v[0]n'est pas un itérateur, de même, push_back()ne prend pas d'itérateur. Donc, du point de vue des juristes linguistiques, votre argument est nul. Désolé. Je sais que la plupart des itérateurs sont des pointeurs, et le point d'invalider un itérateur est à peu près le même que pour les références, mais la partie de la norme que vous citez n'est pas pertinente pour la situation actuelle.
cmaster - réintégrer monica le
-1. C'est une citation totalement hors de propos et n'y répond pas de toute façon. Le comité dit que x.push_back(x[0])c'est SÛR.
Nawaz
0

C'est complètement sûr.

Dans votre deuxième exemple, vous avez

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

ce qui n'est pas nécessaire car si le vecteur sort de sa taille, cela impliquera l'extension reserve.

Vector est responsable de ce truc, pas vous.

Zaffy
la source
-1

Les deux sont sûrs car push_back copiera la valeur, pas la référence. Si vous stockez des pointeurs, c'est toujours sûr en ce qui concerne le vecteur, mais sachez simplement que vous aurez deux éléments de votre vecteur pointant vers les mêmes données.

Section 23.2.1 Exigences générales relatives aux conteneurs

16
  • a.push_back (t) Ajoute une copie de t. Requiert: T doit être CopyInsertable into X.
  • a.push_back (rv) Ajoute une copie de rv. Requiert: T doit être MoveInsertable dans X.

Les implémentations de push_back doivent donc garantir qu'une copie de v[0] est insérée. Par contre-exemple, en supposant une implémentation qui se réallouerait avant la copie, elle n'ajouterait pas assurément une copie de v[0]et, en tant que telle, violerait les spécifications.

OlivierD
la source
2
push_backredimensionnera cependant également le vecteur, et dans une implémentation naïve, cela invalidera la référence avant que la copie ne se produise. Donc, à moins que vous ne puissiez étayer cela par une citation de la norme, je considérerai que c'est faux.
Konrad Rudolph
4
Par «ceci», voulez-vous dire le premier ou le deuxième exemple? push_backcopiera la valeur dans le vecteur; mais (pour autant que je puisse voir) cela pourrait se produire après la réallocation, à quel point la référence à partir de laquelle il essaie de copier n'est plus valide.
Mike Seymour
1
push_backreçoit son argument par référence .
bames53
1
@OlivierD: Il faudrait (1) allouer un nouvel espace (2) copier le nouvel élément (3) déplacer-construire les éléments existants (4) détruire les éléments déplacés (5) libérer l'ancien stockage - dans CET ordre - pour faire fonctionner la première version.
Ben Voigt
1
@BenVoigt pourquoi sinon un conteneur exigerait-il qu'un type soit CopyInsertable s'il va de toute façon ignorer complètement cette propriété?
OlivierD
-2

À partir du 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.

Comme nous l'insérons à la fin, aucune référence ne sera invalidée si le vecteur n'est pas redimensionné. Donc, si le vecteur est capacity() > size()alors il est garanti de fonctionner, sinon il est garanti qu'il s'agit d'un comportement indéfini.

Marque B
la source
Je crois que la spécification garantit en fait que cela fonctionne dans les deux cas. J'attends une référence cependant.
bames53
Il n'y a aucune mention des itérateurs ou de la sécurité des itérateurs dans la question.
OlivierD
3
@OlivierD la partie itérateur est ici superflue: je m'intéresse à la referencespartie de la citation.
Mark B
2
Il est en fait garanti d'être sûr (voir ma réponse, sémantique de push_back).
Angew n'est plus fier de SO