Comportamento estranho com campos de classe ao adicionar a um std :: vector

31

Eu encontrei um comportamento muito estranho (no clang e no GCC) na seguinte situação. Eu tenho um vetor, nodescom um elemento, uma instância de classe Node. Então, chamo uma função nodes[0]que adiciona um novo Nodeao vetor. Quando o novo Nó é adicionado, os campos do objeto de chamada são redefinidos! No entanto, eles parecem voltar ao normal novamente quando a função é concluída.

Eu acredito que este é um exemplo reprodutível 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;
}

Quais saídas

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

Embora você espere que o X permaneça inalterado pelo processo.

Outras coisas que eu tentei:

  • Se eu remover a linha que adiciona um Nodeinterior set(), ela gera X = 3 sempre.
  • Se eu criar um novo Nodee chamar assim ( Node p = nodes[0]), a saída será 3, 3, 3
  • Se eu criar uma referência Nodee chamar isso de ( Node &p = nodes[0]), então a saída será 3, 0, 0 (talvez essa seja porque a referência é perdida quando o vetor é redimensionado?)

Esse comportamento é indefinido por algum motivo? Por quê?

Qq0
fonte
4
Consulte en.cppreference.com/w/cpp/container/vector/push_back . Se você tivesse chamado reserve(2)o vetor antes de chamar set()isso, seria um comportamento definido. Mas escrever uma função como setessa requer que o usuário tenha reservetamanho suficiente antes de chamá-la, a fim de evitar um comportamento indefinido é um design ruim, portanto, não faça isso.
JohnFilleau

Respostas:

39

Seu código tem um comportamento indefinido. No

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

O acesso a Xé realmente this->Xe thisé um ponteiro para o membro do vetor. Quando você nodes.push_back(Node());adiciona um novo elemento ao vetor e esse processo é realocado, o que invalida todos os iteradores, ponteiros e referências a elementos no vetor. Que significa

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

está usando um thisque não é mais válido.

NathanOliver
fonte
Está chamando o push_backcomportamento já indefinido (já que estamos em uma função de membro com invalidação this) ou o UB ocorre na primeira vez que usamos o thisponteiro? Seria possível ou seja return 42;?
n314159 2/03
3
@ n314159 nodesé independente de uma Nodeinstância, portanto, não há UB na chamada push_back. O UB está usando o ponteiro inválido posteriormente.
NathanOliver 02/03
@ n314159 uma boa maneira de conceituar isso é imaginar uma função void set(Node* this), não é indefinido transmitir a ela um ponteiro inválido ou a free()ela na função. Não tenho certeza, mas imagino que mesmo ((Node*) nullptr)->set()seja definido se você não usar thise o método não for virtual.
DutChen18 03/03
Eu não acho que ((Node *) nullptr)->set()esteja bem, pois isso desreferencia um ponteiro nulo (você vê isso claramente quando escrevê-lo de maneira equivalente (*((Node *) nullptr)).set();).
n314159 3/03
11
@Duplicator Atualizei o texto.
NathanOliver 03/03
15
nodes.push_back(Node());

realocará o vetor, alterando o endereço de nodes[0], mas thisnão será atualizado.
tente substituir o setmétodo por 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 como &nodes[0]é diferente depois de ligar push_back.

-fsanitize=addressperceberá isso e até informará em qual linha a memória foi liberada se você também compilar -g.

DutChen18
fonte