Дивна поведінка з полями класів при додаванні до std :: vector


31

Я знайшов дуже дивну поведінку (на кланг та 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]), то вихід 3, 0, 0 (можливо, це тому, що посилання втрачається, коли розмір вектора змінюється?)

Чи є це невизначена поведінка чомусь? Чому?


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який більше не дійсний.


Це виклик push_backвже невизначеної поведінки (оскільки ми тоді перебуваємо в функції члена з недійсною this) чи UB виникає вперше, коли ми використовуємо thisвказівник? Чи можна було б це зробити return 42;?
n314159

3
@ n314159 nodesне залежить від Nodeекземпляра, тому у виклику немає UB push_back. Після цього UB використовує недійсний покажчик.
NathanOliver

@ n314159 Хороший спосіб концептуалізувати це - уявити функцію void set(Node* this), не визначено передати її недійсному вказівнику або free()їй у функції. Я не впевнений, але я гадаю, що навіть ((Node*) nullptr)->set()це визначено, якщо ви не використовуєте thisі метод не є віртуальним.
DutChen18

Я не думаю, що ((Node *) nullptr)->set()це нормально, оскільки це відміняє нульовий вказівник (ви чітко бачите цей корей, коли пишете його рівно як (*((Node *) nullptr)).set();).
n314159

1
@Deduplicator Я оновив формулювання.
NathanOliver

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.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.