Comportement étrange avec les champs de classe lors de l'ajout à un std :: vector

31

J'ai trouvé un comportement très étrange (sur clang et GCC) dans la situation suivante. J'ai un vecteur, nodesavec un élément, une instance de classe Node. J'appelle ensuite une fonction nodes[0]qui ajoute une nouvelle Nodeau vecteur. Lorsque le nouveau nœud est ajouté, les champs de l'objet appelant sont réinitialisés! Cependant, ils semblent redevenir normaux une fois la fonction terminée.

Je pense que c'est un exemple reproductible minimal:

#include <iostream>
#include <vector>

using namespace std;

struct Node;
vector<Node> nodes;

struct Node{
    int X;
    void set(){
        X = 3;
        cout << "Before, X = " << X << endl;
        nodes.push_back(Node());
        cout << "After, X = " << X << endl;
    }
};

int main() {
    nodes = vector<Node>();
    nodes.push_back(Node());

    nodes[0].set();
    cout << "Finally, X = " << nodes[0].X << endl;
}

Quelles sorties

Before, X = 3
After, X = 0
Finally, X = 3

Bien que vous vous attendiez à ce que X reste inchangé par le processus.

J'ai essayé d'autres choses:

  • Si je supprime la ligne qui ajoute un Nodeintérieur set(), elle renvoie X = 3 à chaque fois.
  • Si je crée un nouveau Nodeet l'appelle sur ça ( Node p = nodes[0]) alors la sortie est 3, 3, 3
  • Si je crée une référence Nodeet l'appelle sur ça ( Node &p = nodes[0]) alors la sortie est 3, 0, 0 (peut-être celle-ci est parce que la référence est perdue lorsque le vecteur se redimensionne?)

Ce comportement n'est-il pas défini pour une raison quelconque? Pourquoi?

Qq0
la source
4
Voir en.cppreference.com/w/cpp/container/vector/push_back . Si vous deviez appeler reserve(2)le vecteur avant d'appeler, set()ce serait un comportement défini. Mais écrire une fonction comme setcelle-ci nécessite que l'utilisateur ait reservesuffisamment de taille avant de l'appeler afin d'éviter un comportement indéfini est une mauvaise conception, alors ne le faites pas.
JohnFilleau

Réponses:

39

Votre code a un comportement indéfini. Dans

void set(){
    X = 3;
    cout << "Before, X = " << X << endl;
    nodes.push_back(Node());
    cout << "After, X = " << X << endl;
}

L'accès à Xest vraiment this->Xet thisest un pointeur vers le membre du vecteur. Lorsque vous le faites, nodes.push_back(Node());vous ajoutez un nouvel élément au vecteur et ce processus se réalloue, ce qui invalide tous les itérateurs, pointeurs et références aux éléments du vecteur. Cela signifie

cout << "After, X = " << X << endl;

utilise un thisqui n'est plus valide.

NathanOliver
la source
Appelle le push_backcomportement déjà indéfini (puisque nous sommes alors dans une fonction membre avec invalidé this) ou UB se produit-il la première fois que nous utilisons le thispointeur? Serait-il possible de dire return 42;?
n314159
3
@ n314159 nodesest indépendant d'une Nodeinstance, il n'y a donc pas d'UB dans l'appel push_back. L'UB utilise ensuite le pointeur non valide.
NathanOliver
@ n314159 une bonne façon de conceptualiser ceci est d'imaginer une fonction void set(Node* this), il n'est pas indéfini de lui passer un pointeur invalide, ou de lui free()passer dans la fonction. Je ne suis pas sûr mais j'imagine que même ((Node*) nullptr)->set()est défini si vous n'utilisez pas thiset que la méthode n'est pas virtuelle.
DutChen18
Je ne pense pas que ce ((Node *) nullptr)->set()soit correct, car cela déréférence un pointeur nul (vous voyez clairement ce kore lorsque vous l'écrivez de manière équivalente (*((Node *) nullptr)).set();).
n314159
1
@Deduplicator J'ai mis à jour le libellé.
NathanOliver
15
nodes.push_back(Node());

va réallouer le vecteur, changeant ainsi l'adresse de nodes[0], mais thisn'est pas mis à jour.
essayez de remplacer la setméthode par ce code:

    void set(){
        X = 3;
        cout << "Before, X = " << X << endl;
        cout << "Before, this = " << this << endl;
        cout << "Before, &nodes[0] = " << &nodes[0] << endl;
        nodes.push_back(Node());
        cout << "After, X = " << X << endl;
        cout << "After, this = " << this << endl;
        cout << "After, &nodes[0] = " << &nodes[0] << endl;
    }

notez comment &nodes[0]est différent après avoir appelé push_back.

-fsanitize=addressva attraper cela, et même vous dire sur quelle ligne la mémoire a été libérée si vous compilez également avec -g.

DutChen18
la source