Strano comportamento con i campi di classe quando si aggiunge a uno std :: vector


31

Ho trovato un comportamento molto strano (su clang e GCC) nella seguente situazione. Ho un vettore, nodescon un elemento, un'istanza di classe Node. Quindi chiamo una funzione nodes[0]che aggiunge una nuova Nodeal vettore. Quando viene aggiunto il nuovo nodo, i campi dell'oggetto chiamante vengono resettati! Tuttavia, sembrano tornare alla normalità una volta terminata la funzione.

Credo che questo sia un esempio riproducibile minimo:

#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;
}

Quali uscite

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

Anche se ti aspetteresti che X rimanga invariato dal processo.

Altre cose che ho provato:

  • Se rimuovo la linea che aggiunge un Nodeinterno set(), produce sempre X = 3 ogni volta.
  • Se ne creo uno nuovo Nodee lo chiamo su quello ( Node p = nodes[0]), l'output è 3, 3, 3
  • Se creo un riferimento Nodee lo chiamo su quello ( Node &p = nodes[0]), l'output è 3, 0, 0 (forse questo è perché il riferimento viene perso quando il vettore viene ridimensionato?)

Questo comportamento indefinito è per qualche motivo? Perché?


4
Vedi en.cppreference.com/w/cpp/container/vector/push_back . Se avessi chiamato reserve(2)il vettore prima di chiamare set()questo sarebbe definito comportamento. Ma scrivere una funzione del genere setrichiede che l'utente abbia reservedimensioni sufficienti prima di chiamarlo per evitare comportamenti indefiniti, è una cattiva progettazione, quindi non farlo.
JohnFilleau

Risposte:


39

Il tuo codice ha un comportamento indefinito. In

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

L'accesso a Xè davvero this->Xed thisè un puntatore al membro del vettore. Quando lo fai nodes.push_back(Node());aggiungi un nuovo elemento al vettore e tale processo viene riallocato, il che invalida tutti gli iteratori, i puntatori e i riferimenti agli elementi nel vettore. Questo significa

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

sta usando un thisnon più valido.


Chiamare il push_backcomportamento già indefinito (dal momento che siamo in una funzione membro con invalidata this) o UB si verifica la prima volta che utilizziamo il thispuntatore? Sarebbe possibile cioè return 42;?
n314159

3
@ n314159 nodesè indipendente da Nodeun'istanza, quindi non è presente alcun UB nella chiamata push_back. L'UB utilizza successivamente il puntatore non valido.
NathanOliver

@ n314159 un buon modo per concettualizzare questo è immaginare una funzione void set(Node* this), non è indefinito passargli un puntatore non valido, o ad free()esso nella funzione. Non sono sicuro, ma immagino che ((Node*) nullptr)->set()sia definito anche se non lo usi thise il metodo non è virtuale.
DutChen18

Non penso che ((Node *) nullptr)->set()vada bene, dal momento che questo dereferenzia un puntatore nullo (vedi che kore chiaramente quando lo scrivi in ​​modo equivalente (*((Node *) nullptr)).set();).
n314159

1
@Deduplicator Ho aggiornato il testo.
NathanOliver

15
nodes.push_back(Node());

riallocerà il vettore, modificando così l'indirizzo di nodes[0], ma thisnon verrà aggiornato.
prova a sostituire il setmetodo con questo codice:

    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;
    }

nota come &nodes[0]è diverso dopo aver chiamato push_back.

-fsanitize=addresslo catturerà e ti dirà anche su quale linea è stata liberata la memoria se compili anche -g.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.