Comportamiento extraño con campos de clase cuando se agrega a un std :: vector

31

He encontrado un comportamiento muy extraño (en clang y GCC) en la siguiente situación. Tengo un vector, nodescon un elemento, una instancia de clase Node. Luego llamo a una función nodes[0]que agrega un nuevo Nodeal vector. Cuando se agrega el nuevo nodo, los campos del objeto que llama se restablecen. Sin embargo, parecen volver a la normalidad una vez que la función ha finalizado.

Creo que este es un ejemplo reproducible mínimo:

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

Que salidas

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

Aunque esperarías que X permanezca sin cambios por el proceso.

Otras cosas que he probado:

  • Si elimino la línea que agrega un Nodeinterior set(), genera X = 3 cada vez.
  • Si creo un nuevo Nodey lo llamo en ese ( Node p = nodes[0]), la salida es 3, 3, 3
  • Si creo una referencia Nodey la llamo en ese ( Node &p = nodes[0]), entonces la salida es 3, 0, 0 (¿quizás esta es porque la referencia se pierde cuando el vector cambia de tamaño?)

¿Es este comportamiento indefinido por alguna razón? ¿Por qué?

Qq0
fuente
44
Consulte en.cppreference.com/w/cpp/container/vector/push_back . Si hubiera llamado reserve(2)al vector antes de llamarlo, set()este sería un comportamiento definido. Pero escribir una función como setesa requiere que el usuario reservetenga el tamaño adecuado antes de llamarla para evitar un comportamiento indefinido, es un mal diseño, así que no lo haga.
JohnFilleau

Respuestas:

39

Su código tiene un comportamiento indefinido. En

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

El acceso a Xes realmente this->Xy thises un puntero al miembro del vector. Cuando lo hace nodes.push_back(Node());, agrega un nuevo elemento al vector y ese proceso se reasigna, lo que invalida todos los iteradores, punteros y referencias a elementos en el vector. Eso significa

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

está utilizando un thisque ya no es válido.

NathanOliver
fuente
¿Llamar al push_backcomportamiento ya indefinido (ya que estamos en una función miembro con invalidado this) o UB ocurre la primera vez que usamos el thispuntero? ¿Sería posible es decir return 42;?
n314159
3
@ n314159 nodeses independiente de una Nodeinstancia, por lo que no hay UB en las llamadas push_back. El UB está usando el puntero inválido después.
NathanOliver
@ n314159 una buena manera de conceptualizar esto es imaginar una función void set(Node* this), no está indefinido pasarle un puntero no válido o free()en la función. No estoy seguro, pero imagino que incluso ((Node*) nullptr)->set()se define si no se usa thisy el método no es virtual.
DutChen18
No creo que ((Node *) nullptr)->set()esté bien, ya que esto hace referencia a un puntero nulo (se ve claramente cuando se escribe de manera equivalente como (*((Node *) nullptr)).set();).
n314159
1
@Dupuplicator Actualicé la redacción.
NathanOliver
15
nodes.push_back(Node());

reasignará el vector, cambiando así la dirección de nodes[0], pero thisno se actualiza.
intente reemplazar el setmétodo con este código:

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

observe cómo &nodes[0]es diferente después de llamar push_back.

-fsanitize=addresscaptará esto e incluso le dirá en qué línea se liberó la memoria si también compila -g.

DutChen18
fuente