Références en c++

a marqué ce sujet comme résolu.

Bonjour à tous, j’ai rencontré un problème très intriguant avec les références en c++, voici un bout de code pour le reproduire :

#include <iostream>
#include <vector>

struct Item{
    int i;
};

struct Cont {
    Item& item;
};


int main()
{
    int s = 5;
    std::vector<Item> items;
    std::vector<Cont> conts;

    for (int i=0; i<s; ++i) {
        items.push_back(Item{i});
        conts.push_back(Cont{items[i]});
    }
    
    for (int i=0; i<s; ++i) {
        std::cout << std::addressof(items[i]) << ' ' << items[i].i << std::endl << std::addressof(conts[i].item) << ' ' << conts[i].item.i << std::endl << std::endl;
    }
    
    return 0;
}

/*
output :

0x1b406c0 0
0x1b40b20 -674317320

0x1b406c4 1
0x1b40b64 0

0x1b406c8 2
0x1b40b48 -674317448

0x1b406cc 3
0x1b40b4c 32555

0x1b406d0 4
0x1b406d0 4
*/

Cont contenant une référence à Item, les adresses devraient être les mêmes, mais ici ce n’est pas le cas, sauf pour le dernier élément…

Par contre si je remplis le vecteurs dans 2 boucles séparées, le résultat est bon :

#include <iostream>
#include <vector>

struct Item{
    int i;
};

struct Cont {
    Item& item;
};


int main()
{
    int s = 5;
    std::vector<Item> items;
    std::vector<Cont> conts;

    for (int i=0; i<s; ++i) {
        items.push_back(Item{i});
    }
    for (int i=0; i<s; ++i) {
        conts.push_back(Cont{items[i]});
    }
    
    for (int i=0; i<s; ++i) {
        std::cout << std::addressof(items[i]) << ' ' << items[i].i << std::endl << std::addressof(conts[i].item) << ' ' << conts[i].item.i << std::endl << std::endl;
    }
    
    return 0;
}

/*
output :

0x118ab60 0
0x118ab60 0

0x118ab64 1
0x118ab64 1

0x118ab68 2
0x118ab68 2

0x118ab6c 3
0x118ab6c 3

0x118ab70 4
0x118ab70 4
*/

Autre curiosité, si j’utilise les deux manières de remplir mes vecteurs,les valeurs de i sont correctes sauf pour i=0 et i=1, mais les adresses ne matchent pas :

#include <iostream>
#include <vector>

struct Item{
    int i;
};

struct Cont {
    Item& item;
};


int main()
{
    int s = 5;
    std::vector<Item> items;
    std::vector<Cont> conts;

    
    for (int i=0; i<s; ++i) {
        items.push_back(Item{i});
    }
    for (int i=0; i<s; ++i) {
        conts.push_back(Cont{items[i]});
    }
    
    for (int i=0; i<s; ++i) {
        items.push_back(Item{i});
        conts.push_back(Cont{items[i]});
    }
    
    for (int i=0; i<2*s; ++i) {
        std::cout << std::addressof(items[i]) << ' ' << items[i].i << std::endl << std::addressof(conts[i].item) << ' ' << conts[i].item.i << std::endl << std::endl;
    }
    
    return 0;
}

/*
output :

0x22aa740 0
0x22aab60 36349616

0x22aa744 1
0x22aab64 0

0x22aa748 2
0x22aab68 2

0x22aa74c 3
0x22aab6c 3

0x22aa750 4
0x22aab70 4

0x22aa754 0
0x22aab60 36349616

0x22aa758 1
0x22aab64 0

0x22aa75c 2
0x22aab68 2

0x22aa760 3
0x22aa74c 3

0x22aa764 4
0x22aa750 4
*/

Bref je suis complètement perdu, quelqu’un aurait-il une explication à tout ça ? merci d’avance !

+0 -0

Au pifomètre, je dirais qu’il y a une copie qui se fait quelque part et que du coup, la référence dans ton Cont pointe vers un objet temporaire (et qui n’existe plus une fois que tu sors de la boucle). Tu te retrouve avec un comportement indéterminé, ce qui peut complètement expliquer qu’en séparant la boucle en deux, ça semble fonctionner. Un moyen de tester ça serait de désactiver le constructeur de copie Cont(const Cont&) = delete; et de voir si le compilateur te dit qu’il en a besoin.

De manière générale, c’est assez rare de voir des références dans les membres d’une classe et c’est plus courant d’utiliser un pointeur (qui ne risque pas d’avoir ce genre de problèmes).

Pour compléter la réponse de Berdes, la copie a effectivement lieu lors de la réallocation automatique du vector d’items. Ici, apparament, lors de l’ajout du 5ème élément.

Les premiers éléments de conts sont donc des références mortes… et changer la référence en pointeur ne règlera pas le problème.

Pour le confirmer, ajoute un reserve avant le début de la boucle.

En fait, c’est très dangereux de faire référence directement à un élément qui se trouve dans un conteneur, car il peut être copié ou déplacé à tout moment sans que tu ne le saches, et en fait tu n’as pas à le savoir. Il est vivement recommandé d’utiliser plutôt un conteneur de pointeurs intelligents, ici par exemple vector<unique/shared_ptr<Item>>.

+0 -0

En fait, c’est très dangereux de faire référence directement à un élément qui se trouve dans un conteneur, car il peut être copié ou déplacé à tout moment sans que tu ne le saches, et en fait tu n’as pas à le savoir. Il est vivement recommandé d’utiliser plutôt un conteneur de pointeurs intelligents, ici par exemple vector<unique/shared_ptr<Item>>.

QuentinC

Utiliser un tableau de pointeurs plutôt qu’un tableau d’objets, c’est pas une décision triviale : on va casser la mémoire cache comme ça. Et l’utilisation de pointeurs n’est pas forcément requis par la sémantique de la classe (par exemple s’il y a un héritage et un risque de slicing).

Ce n’est pas "sans que tu ne le saches", c’est précisé dans la documentation si une fonction peut invalider les éléments ou pas. Par exemple pour push_back :

If the new size() is greater than capacity() then all iterators and references (including the past-the-end iterator) are invalidated. Otherwise only the past-the-end iterator is invalidated. source : https://en.cppreference.com/w/cpp/container/vector/push_back

Donc on peut avoir des indirections sur les éléments d’un tableau, il faut juste faire attention. (Je pense que -Wlifetime doit détecter ce genre d’erreur. À vérifier)

std::vector<int> v = { ... };
int& r = v.front();
int* p = &v[0];
iterator i = v.begin();
v.push_back(123);
r; // peut être invalide
p; // peut être invalide
i; // peut être invalide
+0 -0

Utiliser un tableau de pointeurs plutôt qu’un tableau d’objets, c’est pas une décision triviale : on va casser la mémoire cache comme ça.

Certes, si tu fais un moteur 3D, une VM de langage ou que tu es dans une zone critique du genre, d’accord. Mais sinon en règle générale ce n’est pas primordial.

A mon avis si à un moment donné tu dois prendre des références sur un objet, c’est que tu as nécessairement une logique d’entité. IL s’en suit que tu dois manipuler uniquement des pointeurs et des références partout dans ton code. Quand on a une logique de valeur, on copie systématiquement; on ne se casse jamais les pieds à prendre des références.

Donc on peut avoir des indirections sur les éléments d’un tableau, il faut juste faire attention.

ca c’est la réponse d’expert: on peut toujours tout faire, si on fait attention.

Mais en règle générale, ce n’est pas parce que quelque chose est possible que c’est forcément une bonne idée. Surtout en C++ où on a plus de chances de faire n’importe quoi qu’autre chose. Les autres langages nous offrent généralement moins de possibilités de se tirer dans les pieds.

Ce n’est pas "sans que tu ne le saches", c’est précisé dans la documentation si une fonction peut invalider les éléments ou pas.

En effet, mais je me suis peut-être mal exprimé.

JE voulais dire que c’est censé être un détail d’implémentation: il ne faut pas s’y fier, il faut supposer que le comportement peut changer. ON met des éléments dans un conteneur, c’est lui qui les gère comme il l’entend et on n’a pas à s’intéresser aux détails. Tout ce qui nous intéresse c’est de pouvoir les retrouver.

Pour vector c’est assez facile de savoir ce qui se passe, mais ça serait autrement plus compliqué avec un set, une map, ou autre… même si c’est parfaitement documenté.

JE t’assure que depuis que j’utilise à peu près systématiquement des pointeurs intelligents, j’ai beaucoup moins de problèmes. C’est peut-être un tout petit peu moins bon en terme de performances, mais c’est infiniment plus sûr.

+0 -0

C’est pas réellement une question d’utiliser des pointeurs intelligents, mais plus une question d’ordre d’apprentissage. A mon sens, les indirections et le fait qu’elles peuvent devenir invalides est une notion qui est plus fondamentale que le cas particulier des pointeurs intelligents (qui sont des indirections). Et donc cette notion être comprise avant, dans l’ordre d’apprentissage. Du coup, je pars du principe que si on sait utiliser l’approche avec les pointeurs intelligents dans un vector, on devrait connaitre ce probleme d’invalidation des indirections sur les éléments d’un vector.

Je comprends tes arguments, mais utiliser des pointeurs si c’est pour éviter d’être confronté a ce probleme d’invalidation des indirections, cela me semble plus problématique au final. En tout cas, ca pourrait etre le signe d’un probleme dans l’apprentissage, a mon sens.

+0 -0

je pars du principe que si on sait utiliser l’approche avec les pointeurs intelligents dans un vector, on devrait connaitre ce probleme d’invalidation des indirections sur les éléments d’un vector.

Je ne sais pas si c’est parce que mon langage de prédilection est Java, mais au contraire je suis partisan d’une approche top down: on apprend à utiliser la bibliothèque standard dans ses grandes lignes, et seulement après on en apprend ses petites subtilités. ET pour moi les règles d’invalidation des itérateurs font partie des subtilités, car on n’a réellement que très rarement à s’en soucier.

Du coup je ne suis vraiment pas convaincu que ce soit réellement le cas en général.

+0 -0

Je suis pour aussi les approches top-down pour l’apprentissage. Entre des pointeurs nus ou des pointeurs intelligents, je conseille de commencer par les secondes. Entre des tableaux du C ou des tableaux de la lib standard, idem.

Parce que dans ces exemple, on a des solutions équivalentes (qui permettent de faire la même chose), la version "moderne" étant plus safe pour un surcoût acceptable.

Mais en C++, on ne peut pas échapper aux problèmes de gestion de la mémoire. (Voire, c’est ce qui fait la spécificité et donc l’intérêt du C++). Comme on a un contrôle explicite de l’ownership et du lifetime (contrairement au Java), ce sont des notions qu’il faut apprendre. Les problèmes d’invalidation d’une indirection apparaissent par exemple dès que l’on va essayer de retourner une référence depuis une fonction.

Les approches d’un tableau de valeurs (pour lequel il faut faire attention aux invalidations d’indirections) et un tableau de pointeurs ne sont pas des solutions équivalentes. Ce sont 2 approches différentes, qui ont des avantages et des défauts chacune. L’une n’est pas plus top ou down que l’autre. Je mets la première solution avant dans l’apprentissage uniquement parce que le probleme d’invalidation des indirections va apparaître avant, lorsque l’on va voir les fonctions. (Je mets l’apprentissage des pointeurs assez tard, quand on en a réellement besoin, c’est a dire lors de l’apprentissage du polymorphisme d’héritage).

C’est pour cela que dans cette discussion, ou la personne teste explicitement les problèmes d’invalidation d’indirection dans un vector, l’approche avec un vector de pointeur n’est pas une réponse, puisque c’est une autre approche et qu’il faut qu’il comprenne ce probleme de validité.

+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte