Seltsames Verhalten mit Klassenfeldern beim Hinzufügen zu einem std :: vector

31

Ich habe in der folgenden Situation ein sehr seltsames Verhalten (bei Clang und GCC) festgestellt. Ich habe einen Vektor nodesmit einem Element, einer Klasseninstanz Node. Ich rufe dann eine Funktion auf nodes[0], die Nodedem Vektor eine neue hinzufügt . Wenn der neue Knoten hinzugefügt wird, werden die Felder des aufrufenden Objekts zurückgesetzt! Sie scheinen jedoch nach Beendigung der Funktion wieder normal zu werden.

Ich glaube, dies ist ein minimal reproduzierbares Beispiel:

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

Welche Ausgänge

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

Sie würden jedoch erwarten, dass X durch den Prozess unverändert bleibt.

Andere Dinge, die ich versucht habe:

  • Wenn ich die Zeile entferne, die ein NodeInneres hinzufügt set(), wird jedes Mal X = 3 ausgegeben.
  • Wenn ich ein neues erstelle Nodeund es auf dem ( Node p = nodes[0]) aufrufe, ist die Ausgabe 3, 3, 3
  • Wenn ich eine Referenz erstelle Nodeund diese ( Node &p = nodes[0]) aufrufe, ist die Ausgabe 3, 0, 0 (möglicherweise liegt dies daran, dass die Referenz verloren geht, wenn die Größe des Vektors geändert wird?)

Ist das undefiniertes Verhalten aus irgendeinem Grund? Warum?

Qq0
quelle
4
Siehe en.cppreference.com/w/cpp/container/vector/push_back . Wenn Sie reserve(2)den Vektor vor dem Aufruf aufgerufen set()hätten, wäre dies ein definiertes Verhalten. Das Schreiben einer solchen Funktion seterfordert jedoch, dass der Benutzer reservevor dem Aufrufen eine ausreichende Größe hat, um undefiniertes Verhalten zu vermeiden. Dies ist ein schlechtes Design. Tun Sie es also nicht.
JohnFilleau

Antworten:

39

Ihr Code hat ein undefiniertes Verhalten. Im

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

Der Zugriff auf Xist wirklich this->Xund thisist ein Zeiger auf das Mitglied des Vektors. Wenn Sie dies tun nodes.push_back(Node());, fügen Sie dem Vektor ein neues Element hinzu, und dieser Prozess ordnet ihn neu zu, wodurch alle Iteratoren, Zeiger und Verweise auf Elemente im Vektor ungültig werden. Das bedeutet

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

verwendet eine this, die nicht mehr gültig ist.

NathanOliver
quelle
Ruft das push_backbereits undefinierte Verhalten auf (da wir uns dann in einer Member-Funktion mit ungültig machen this) oder tritt UB auf, wenn wir den thisZeiger zum ersten Mal verwenden ? Wäre es möglich, dh return 42;?
n314159
3
@ n314159 nodesist unabhängig von einer NodeInstanz, sodass kein UB aufgerufen wird push_back. Die UB verwendet anschließend den ungültigen Zeiger.
NathanOliver
@ n314159 Eine gute Möglichkeit, dies zu konzipieren, besteht darin, sich eine Funktion vorzustellen void set(Node* this). Es ist nicht undefiniert, ihr einen ungültigen Zeiger oder free()in der Funktion zu übergeben. Ich bin nicht sicher, aber ich stelle mir vor, dass sogar ((Node*) nullptr)->set()definiert wird, wenn Sie nicht verwenden thisund die Methode nicht virtuell ist.
DutChen18
Ich denke nicht, dass ((Node *) nullptr)->set()das in Ordnung ist, da dies einen Nullzeiger dereferenziert (Sie sehen das deutlich, wenn Sie es gleichwertig schreiben als (*((Node *) nullptr)).set();).
n314159
1
@Deduplicator Ich habe den Wortlaut aktualisiert.
NathanOliver
15
nodes.push_back(Node());

wird den Vektor neu zuweisen, wodurch die Adresse von geändert wird nodes[0], aber thisnicht aktualisiert wird.
Versuchen Sie, die setMethode durch diesen Code zu ersetzen :

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

Beachten Sie, wie &nodes[0]anders nach dem Anruf ist push_back.

-fsanitize=addresswird dies abfangen und Ihnen sogar sagen, in welcher Zeile der Speicher freigegeben wurde, wenn Sie auch mit kompilieren -g.

DutChen18
quelle