Question Pourquoi les éléments de copie stl :: list sont-ils ajoutés à la liste?


La documentation de la bibliothèque de modèles standard pour liste dit:

annuler push_back (const T & x);

Ajouter un élément à la fin Ajoute un nouvel élément à la fin de la liste,   juste après son dernier élément actuel. Le contenu de ce nouvel élément   est initialisé à une copie de x.

Ces sémantiques diffèrent grandement de la sémantique de Java et me confondent. Y a-t-il un principe de conception dans la STL qui me manque? "Copier les données tout le temps"? Cela me fait peur. Si j'ajoute une référence à un objet, pourquoi l'objet est-il copié? Pourquoi l'objet n'est-il pas simplement transmis?

Il doit y avoir une décision de conception de langage ici, mais la plupart des commentaires que j'ai trouvés sur Stack Overflow et sur d'autres sites se concentrent sur les problèmes de levée associés au fait que toute cette copie d'objet peut générer des exceptions. Si vous ne copiez pas et gérez simplement les références, tous ces problèmes d'exception disparaissent. Très confus.

Remarque: dans cette base de code héritée avec laquelle je travaille, Boost n'est pas une option.


10
2017-10-16 14:05


origine


Réponses:


Le STL stocke toujours exactement ce que vous lui dites de stocker. list<T> est toujours une liste de T donc tout sera stocké à la valeur. Si vous voulez une liste de pointeurs, utilisez list<T*>, qui sera similaire à la sémantique en Java.

Cela pourrait vous inciter à essayer list<T&>, mais ce n'est pas possible. Les références en C ++ ont une sémantique différente de celle des références en Java. En C ++, les références doivent être initialisées pour pointer sur un objet. Une fois qu'une référence est initialisée, elle pointe toujours vers cet objet. Vous ne pouvez jamais le faire pointer vers un objet différent. Cela rend impossible d'avoir un conteneur de références en C ++. Les références Java sont plus étroitement liées aux pointeurs C ++, vous devez donc utiliser list<T*>, au lieu.


11
2017-10-16 14:12



C'est ce qu'on appelle la "sémantique de la valeur". C ++ est normalement codé pour copier des valeurs, contrairement à Java où, à part les types primitifs, vous copiez des références. Cela peut vous faire peur mais personnellement, la sémantique de référence de Java me fait plus peur. Mais en C ++, vous avez le choix, si vous voulez que la sémantique de référence utilise simplement des pointeurs (de préférence intelligents). Ensuite, vous serez plus proche du Java auquel vous êtes habitué. Mais rappelez-vous que dans C ++, il n'y a pas de récupération de mémoire (c'est pourquoi vous devriez normalement utiliser des pointeurs intelligents).


6
2017-10-16 14:07



Vous n’ajoutez pas de référence à un objet. Vous passez un objet par référence. C'est différent. Si vous n'avez pas réussi par référence, une copie supplémentaire peut avoir été faite avant l'insertion réelle.

Et il fait une copie parce que vous avez besoin d'une copie, sinon un code comme:

std::list<Obj> x;
{
   Obj o;
   x.insert(o);
}

laisserait la liste avec un objet invalide, car o est sorti de la portée. Si vous voulez quelque chose de similaire à Java, pensez à utiliser shared_ptr. Cela vous offre les avantages auxquels vous êtes habitué en Java: la gestion automatique de la mémoire et la copie légère.


4
2017-10-16 14:09



Java fonctionne de la même manière, en fait. Permettez-moi d'expliquer:

Object obj = new Object();
List<Object> list = new LinkedList<Object>();
list.add(obj);

Quel est le type de obj? C'est un référence à un Object. L'objet réel flotte quelque part sur le tas - la seule chose que vous pouvez faire en Java est de lui transmettre des références. Vous passez une référence à l'objet à la liste addméthode, et la liste enregistre un copie de cette référence en soi. Vous pouvez modifier ultérieurement la référence nommée obj sans affecter la copie séparée de cette référence stockée dans la liste. (Bien sûr, si vous modifiez l'objet lui-même, vous pouvez voir ce changement via l'une ou l'autre des références.)

C ++ a plus d'options. Vous pouvez émuler Java:

class Object {};
// ...
Object* obj = new Object;
std::list<Object*> list;
list.push_back(obj);

Quel est le type de obj? C'est un pointeur vers un Object. Lorsque vous le passez à la liste push_back méthode, la liste enregistre un copie de ce pointeur en soi. Cela a la même sémantique que Java.

Mais si vous y réfléchissez d'un point de vue de l'efficacité… quelle est la taille d'un pointeur C ++ / référence Java? 4 octets ou 8 octets, selon votre architecture. Si l’objet dont vous vous souciez est de cette taille ou plus petit, pourquoi ne pas le mettre sur le tas et y passer des pointeurs partout? Passez simplement l'objet:

class Object {};
// ...
Object obj;
std::list<Object> list;
list.push_back(obj);

À présent, obj est un objet réel. Vous le passez à la liste push_back méthode, qui stocke un copie de cet objet en soi. Ceci est un idiome C ++, en quelque sorte. Non seulement cela a-t-il du sens pour les petits objets, où un pointeur est pur, mais il facilite aussi les choses dans un langage non-GC (il n'y a rien qui puisse couler accidentellement) et si la durée de vie de l'objet est naturellement liée à la liste (c.-à-d. si elle est supprimée de la liste, puis sémantiquement, elle ne devrait plus exister), vous pourriez aussi bien stocker l'objet entier dans la liste. Il a également des avantages de cache locality (lorsqu'il est utilisé dans un std::vector, en tous cas).


Vous pouvez demander: "Pourquoi push_back prenez un argument de référence, alors? "Il y a une raison assez simple à cela. Chaque paramètre est passé par valeur (encore une fois en C ++ et en Java). Si vous avez un std::list de Object*, bien, vous passez votre pointeur et un copie de ce pointeur est fait et passé dans le push_back fonction. Ensuite, dans cette fonction, une autre copie de ce pointeur est faite et stockée dans le conteneur.

C'est bien pour un pointeur. Mais en C ++, la copie d'objets peut être compliquée de manière arbitraire. Un constructeur de copie peut faire n'importe quoi. Dans certaines circonstances, copier un objet deux fois (une fois dans la fonction et à nouveau dans le conteneur) peut être un problème de performance. Alors push_back prend son argument par référence const: il crée une copie unique, directement à partir de l'objet d'origine dans le conteneur.


4
2017-10-16 14:21



Sans le comptage des références, il n'y a aucun moyen de conserver la propriété partagée, de sorte que la propriété individuelle est maintenue par la copie.

Considérez le cas courant où vous souhaitez ajouter un objet alloué à la pile à une liste qui lui survit:

void appendHuzzah(list<string> &strs) {
  strs.push_back(string("huzzah!"));
}

La liste ne peut pas conserver l'objet d'origine car cet objet sera détruit lorsqu'il est hors de portée. En copiant, la liste obtient son propre objet dont la durée de vie est entièrement sous son contrôle. S'il en était autrement, une telle utilisation simple tomberait en panne et serait inutile et nous le ferions toujours doivent utiliser des listes de pointeurs.

Java distingue les types primitifs et les types de référence. En C ++, tous les types sont primitifs.


1
2017-11-23 06:26