Hành vi kỳ lạ với các trường lớp khi thêm vào một std :: vector


31

Tôi đã tìm thấy một số hành vi rất kỳ lạ (trên clang và GCC) trong tình huống sau đây. Tôi có một vectơ, nodesvới một phần tử, một thể hiện của lớp Node. Sau đó tôi gọi một hàm trên nodes[0]đó thêm một cái mới Nodevào vector. Khi Node mới được thêm vào, các trường của đối tượng gọi được đặt lại! Tuy nhiên, chúng dường như trở lại bình thường một lần nữa khi chức năng kết thúc.

Tôi tin rằng đây là một ví dụ có thể tái tạo tối thiểu:

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

Đầu ra nào

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

Mặc dù bạn sẽ mong đợi X không thay đổi theo quy trình.

Những thứ khác tôi đã thử:

  • Nếu tôi loại bỏ dòng có thêm một Nodebên trong set(), thì nó sẽ xuất X = 3 mỗi lần.
  • Nếu tôi tạo một cái mới Nodevà gọi nó trên đó ( Node p = nodes[0]) thì đầu ra là 3, 3, 3
  • Nếu tôi tạo một tham chiếu Nodevà gọi nó trên đó ( Node &p = nodes[0]) thì đầu ra là 3, 0, 0 (có lẽ điều này là do tham chiếu bị mất khi vectơ thay đổi kích thước?)

Đây có phải là hành vi không xác định vì một số lý do? Tại sao?


4
Xem en.cppreference.com/w/cpp/container/vector/push_back . Nếu bạn đã gọi reserve(2)vectơ trước khi gọi set()thì đây sẽ là hành vi được xác định. Nhưng việc viết một hàm như thế setđòi hỏi người dùng phải reservecó kích thước phù hợp trước khi gọi nó để tránh hành vi không xác định là thiết kế xấu, vì vậy đừng làm điều đó.
JohnFilleau

Câu trả lời:


39

Mã của bạn có hành vi không xác định. Trong

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

Truy cập vào Xthực sự this->Xthislà một con trỏ đến thành viên của vectơ. Khi bạn làm, nodes.push_back(Node());bạn thêm một phần tử mới vào vectơ và quá trình phân bổ lại, làm mất hiệu lực tất cả các trình vòng lặp, con trỏ và tham chiếu đến các phần tử trong vectơ. Điều đó có nghĩa là

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

đang sử dụng một thiskhông còn hiệu lực.


Việc gọi push_backhành vi đã không xác định (vì khi đó chúng ta đang ở trong hàm thành viên bị vô hiệu this) hoặc UB có xảy ra lần đầu tiên khi chúng ta sử dụng thiscon trỏ không? Nó sẽ có thể tức là return 42;?
n314159

3
@ n314159 nodesđộc lập với một Nodethể hiện nên không có UB trong cuộc gọi push_back. UB đang sử dụng con trỏ không hợp lệ sau đó.
NathanOliver

@ n314159 một cách tốt để khái niệm hóa điều này là tưởng tượng một hàm void set(Node* this), nó không được xác định để truyền cho nó một con trỏ không hợp lệ hoặc cho free()nó trong hàm. Tôi không chắc nhưng tôi tưởng tượng rằng thậm chí ((Node*) nullptr)->set()được xác định nếu bạn không sử dụng thisvà phương thức này không phải là ảo.
DutChen18 ngày

Tôi không nghĩ điều đó ((Node *) nullptr)->set()là ổn, vì các điều khoản này là một con trỏ null (bạn thấy rõ điều đó khi viết nó tương đương như (*((Node *) nullptr)).set();).
n314159 ngày

1
@Ded repeatator Tôi cập nhật từ ngữ.
NathanOliver ngày

15
nodes.push_back(Node());

sẽ phân bổ lại vectơ, do đó thay đổi địa chỉ của nodes[0], nhưng thiskhông được cập nhật.
hãy thử thay thế setphương thức bằng mã này:

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

lưu ý như thế nào &nodes[0]là khác nhau sau khi gọi push_back.

-fsanitize=addresssẽ nắm bắt được điều này và thậm chí cho bạn biết bộ nhớ nào đã được giải phóng nếu bạn cũng biên dịch -g.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.