Странное поведение с полями классов при добавлении в std :: vector

31

Я обнаружил очень странное поведение (на Clang и GCC) в следующей ситуации. У меня есть вектор, nodesс одним элементом, экземпляр класса Node. Затем я вызываю функцию, nodes[0]которая добавляет новый Nodeвектор. При добавлении нового узла поля вызывающего объекта сбрасываются! Однако они, похоже, снова возвращаются в нормальное состояние после завершения функции.

Я считаю, что это минимальный воспроизводимый пример:

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

Какие выводы

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

Хотя вы ожидаете, что X останется неизменным в процессе.

Другие вещи, которые я пробовал:

  • Если я удаляю строку, которая добавляет Nodeвнутреннюю часть set(), то каждый раз выводится X = 3.
  • Если я создаю новый Nodeи вызываю его на то ( Node p = nodes[0]), то результат будет 3, 3, 3
  • Если я создаю ссылку Nodeи вызываю ее для Node &p = nodes[0]this ( ), то вывод будет равен 3, 0, 0 (возможно, это происходит потому, что ссылка теряется при изменении размера вектора?)

Это неопределенное поведение по какой-то причине? Почему?

Qq0
источник
4
См. En.cppreference.com/w/cpp/container/vector/push_back . Если бы вы вызывали reserve(2)вектор перед вызовом, set()это было бы определенным поведением. Но написание подобной функции setтребует от пользователя reserveдостаточно подходящего размера перед вызовом, чтобы избежать неопределенного поведения, и это плохой дизайн, поэтому не делайте этого.
JohnFilleau

Ответы:

39

Ваш код имеет неопределенное поведение. В

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

Доступ к Xдействительно this->Xи thisявляется указателем на член вектора. Когда вы nodes.push_back(Node());добавляете новый элемент в вектор, и этот процесс перераспределяет, который делает недействительными все итераторы, указатели и ссылки на элементы в векторе. Это значит

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

использует, thisкоторый больше не действителен.

NathanOliver
источник
Вызывает push_backуже неопределенное поведение (поскольку мы находимся в функции-члене с недействительными this) или UB возникает при первом использовании thisуказателя? Будет ли это возможно return 42;?
n314159
3
@ n314159 nodesне зависит от Nodeэкземпляра, поэтому в вызове нет UB push_back. UB впоследствии использует неверный указатель.
Натан Оливер
@ n314159 Хороший способ осмыслить это - представить функцию void set(Node* this), которая не является неопределенной, чтобы передать ей недопустимый указатель или free()функцию в функции. Я не уверен, но я представляю, что даже ((Node*) nullptr)->set()определяется, если вы не используете, thisи метод не является виртуальным.
DutChen18
Я не думаю, что ((Node *) nullptr)->set()это нормально, так как это разыменовывает нулевой указатель (вы видите это kore ясно, когда пишете это эквивалентно как (*((Node *) nullptr)).set();).
n314159
1
@Deduplicator Я обновил формулировку.
Натан Оливер
15
nodes.push_back(Node());

перераспределяет вектор, изменяя адрес nodes[0], но thisне обновляется.
попробуйте заменить setметод следующим кодом:

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

обратите внимание, как &nodes[0]отличается после звонка push_back.

-fsanitize=addressпоймает это и даже скажет вам, по какой строке была освобождена память, если вы также скомпилируете -g.

DutChen18
источник