Perilaku aneh dengan bidang kelas saat menambahkan ke std :: vector

31

Saya telah menemukan beberapa perilaku yang sangat aneh (pada dentang dan GCC) dalam situasi berikut. Saya punya vektor,, nodesdengan satu elemen, turunan dari kelas Node. Saya kemudian memanggil fungsi nodes[0]yang menambahkan baru Nodeke vektor. Ketika Node baru ditambahkan, bidang objek panggilan direset! Namun, mereka tampaknya kembali normal lagi setelah fungsi selesai.

Saya percaya ini adalah contoh minimal yang dapat direproduksi:

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

Output yang mana

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

Meskipun Anda akan mengharapkan X tetap tidak berubah oleh proses.

Hal lain yang saya coba:

  • Jika saya menghapus baris yang menambahkan bagian Nodedalam set(), maka itu menghasilkan X = 3 setiap kali.
  • Jika saya membuat yang baru Nodedan menyebutnya ( Node p = nodes[0]) maka hasilnya adalah 3, 3, 3
  • Jika saya membuat referensi Nodedan menyebutnya pada itu ( Node &p = nodes[0]) maka hasilnya adalah 3, 0, 0 (mungkin yang ini adalah karena referensi tersebut hilang ketika vektor diubah ukurannya?)

Apakah ini perilaku yang tidak terdefinisi untuk beberapa alasan? Mengapa?

Qq0
sumber
4
Lihat en.cppreference.com/w/cpp/container/vector/push_back . Jika Anda telah memanggil reserve(2)vektor sebelum memanggil set()ini akan didefinisikan perilaku. Tapi menulis fungsi seperti setitu mengharuskan pengguna untuk reserveukuran yang cukup sebelum memanggilnya untuk menghindari perilaku yang tidak jelas adalah desain yang buruk, jadi jangan lakukan itu.
JohnFilleau

Jawaban:

39

Kode Anda memiliki perilaku yang tidak terdefinisi. Di

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

Akses ke Xbenar this->X- benar dan thismerupakan penunjuk ke anggota vektor. Ketika Anda melakukannya, nodes.push_back(Node());Anda menambahkan elemen baru ke vektor dan proses yang realokasi, yang membatalkan semua iterator, pointer dan referensi ke elemen dalam vektor. Itu berarti

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

menggunakan thisyang tidak lagi valid.

NathanOliver
sumber
Apakah memanggil push_backperilaku yang sudah tidak terdefinisi (karena kita kemudian dalam fungsi anggota dengan tidak valid this) atau apakah UB muncul pertama kali kita menggunakan thispointer? Apakah mungkin untuk itu return 42;?
n314159
3
@ n314159 nodesindependen dari Nodeinstance sehingga tidak ada UB yang menelepon push_back. UB menggunakan pointer yang tidak valid setelahnya.
NathanOliver
_____ @ n314159 cara yang baik untuk membuat konsep ini adalah dengan membayangkan suatu fungsi void set(Node* this), tidak terdefinisi untuk memberikannya sebuah pointer yang tidak valid, atau ke free()dalam fungsinya. Saya tidak yakin tetapi saya membayangkan bahwa bahkan ((Node*) nullptr)->set()didefinisikan jika Anda tidak menggunakan thisdan metode ini bukan virtual.
DutChen18
Saya tidak berpikir itu ((Node *) nullptr)->set()ok, karena dereferences ini pointer nol (Anda melihat kore itu dengan jelas ketika menulisnya sama dengan (*((Node *) nullptr)).set();).
n314159
1
@Dupuplikator Saya memperbarui kata-katanya.
NathanOliver
15
nodes.push_back(Node());

akan realokasi vektor, sehingga mengubah alamat nodes[0], tetapi thistidak diperbarui.
coba ganti setmetode dengan kode ini:

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

perhatikan bagaimana &nodes[0]perbedaannya setelah menelepon push_back.

-fsanitize=addressakan menangkap ini, dan bahkan memberi tahu Anda di baris mana memori itu dibebaskan jika Anda juga mengompilasinya -g.

DutChen18
sumber