Références en c++

a marqué ce sujet comme résolu.
Auteur du sujet

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 !

Édité par __Nicolas__

+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).

+0 -0

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

Ma plateforme avec 23 jeux de société classiques en 6 langues et 13000 joueurs: http://qcsalon.net/ | Apprenez à faire des sites web accessibles http://www.openweb.eu.org/

+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

Pour poser des questions ou simplement discuter informatique, vous pouvez rejoindre le discord NaN.

+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