Dziwne zachowanie z polami klasy podczas dodawania do std :: vector

31

Znalazłem bardzo dziwne zachowanie (na clang i GCC) w następującej sytuacji. Mam wektor, nodesz jednym elementem, instancją klasy Node. Następnie wywołuję funkcję, nodes[0]która dodaje nową wartość Nodedo wektora. Po dodaniu nowego węzła pola obiektu wywołującego są resetowane! Wydają się jednak wracać do normy po zakończeniu funkcji.

Uważam, że jest to minimalny, powtarzalny przykład:

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

Które wyjścia

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

Chociaż można oczekiwać, że X pozostanie niezmieniony przez ten proces.

Inne rzeczy, których próbowałem:

  • Jeśli usunę linię, która dodaje Nodewnętrze set(), to za każdym razem wyprowadza X = 3.
  • Jeśli utworzę nowy Nodei wywołam go na tym ( Node p = nodes[0]), wówczas wynikiem będzie 3, 3, 3
  • Jeśli utworzę odwołanie Nodei wywołam go na tym ( Node &p = nodes[0]), wówczas wynikiem będzie 3, 0, 0 (być może jest to spowodowane tym, że odwołanie zostało utracone, gdy wektor zmienia rozmiar?)

Czy z jakiegoś powodu jest to niezdefiniowane zachowanie? Dlaczego?

Pytanie 0
źródło
4
Zobacz en.cppreference.com/w/cpp/container/vector/push_back . Jeśli miałbyś wywołać reserve(2)wektor przed wywołaniem, set()byłoby to zdefiniowane zachowanie. Ale napisanie takiej funkcji setwymaga od użytkownika odpowiedniego reserverozmiaru przed wywołaniem jej, aby uniknąć nieokreślonego zachowania jest złym projektem, więc nie rób tego.
JohnFilleau

Odpowiedzi:

39

Twój kod ma niezdefiniowane zachowanie. W

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

Dostęp do Xjest naprawdę this->Xi thisjest wskaźnikiem do elementu wektora. Gdy to zrobisz nodes.push_back(Node());, dodasz nowy element do wektora, a proces ten zostanie ponownie przydzielony, co unieważnia wszystkie iteratory, wskaźniki i odniesienia do elementów w wektorze. To znaczy

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

używa thisnieważnego.

NathanOliver
źródło
Czy wywoływanie push_backjuż niezdefiniowanego zachowania (ponieważ jesteśmy wtedy w funkcji członka z unieważnioną funkcją this) czy też UB występuje przy pierwszym użyciu thiswskaźnika? Czy byłoby możliwe np. return 42;?
n314159
3
@ n314159 nodesjest niezależny od Nodeinstancji, więc nie ma UB w wywołaniu push_back. UB używa następnie nieprawidłowego wskaźnika.
NathanOliver
@ n314159 dobrym sposobem na konceptualizację tego jest wyobrażenie sobie funkcji void set(Node* this), nie jest niezdefiniowane, aby przekazać jej nieprawidłowy wskaźnik lub do free()funkcji. Nie jestem pewien, ale wyobrażam sobie, że nawet ((Node*) nullptr)->set()jest zdefiniowane, jeśli nie używasz, thisa metoda nie jest wirtualna.
DutChen18
Nie sądzę, żeby to ((Node *) nullptr)->set()było w porządku, ponieważ to wyklucza wskaźnik zerowy (korelię tę wyraźnie widać, gdy pisze się ją równoważnie jako (*((Node *) nullptr)).set();).
n314159
1
@Deduplicator Zaktualizowałem brzmienie.
NathanOliver
15
nodes.push_back(Node());

ponownie przydzieli wektor, zmieniając w ten sposób adres nodes[0], ale thisnie zostanie zaktualizowany.
spróbuj zastąpić setmetodę tym kodem:

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

zwróć uwagę, jak &nodes[0]różni się po telefonowaniu push_back.

-fsanitize=addresszłapie to, a nawet powie, w której linii pamięć została zwolniona, jeśli również się skompilujesz -g.

DutChen18
źródło