2010-11-02 28 views
10

Tôi đang bối rối về trạng thái của một đối tượng sau nó được di chuyển bằng cách sử dụng ngữ nghĩa di chuyển C++ 0x. Sự hiểu biết của tôi là khi một đối tượng đã được di chuyển, nó vẫn là một đối tượng hợp lệ, nhưng trạng thái bên trong của nó đã bị thay đổi để khi destructor của nó được gọi, không có tài nguyên nào được deallocated.Vật thể zombie sau std :: di chuyển

Nhưng nếu hiểu biết của tôi là chính xác, hàm hủy của đối tượng đã di chuyển nên vẫn được gọi.

Nhưng, điều đó không xảy ra khi tôi thực hiện một thử nghiệm đơn giản:

struct Foo 
{ 
    Foo() 
    { 
     s = new char[100]; 
     cout << "Constructor called!" << endl; 
    } 

    Foo(Foo&& f) 
    { 
     s = f.s; 
     f.s = 0; 
    } 

    ~Foo() 
    { 
     cout << "Destructor called!" << endl; 
     delete[] s; // okay if s is NULL 
    } 

    void dosomething() { cout << "Doing something..." << endl; } 

    char* s; 
}; 

void work(Foo&& f2) 
{ 
    f2.dosomething(); 
} 

int main() 
{ 
    Foo f1; 
    work(std::move(f1)); 
} 

sản lượng này:

Constructor called! 
Doing something... 
Destructor called! 

Thông báo destructor chỉ được gọi một lần. Điều này cho thấy rằng sự hiểu biết của tôi ở đây là tắt. Tại sao không phải là destructor được gọi là hai lần? Dưới đây là giải thích của tôi về những gì nên đã xảy ra:

  1. Foo f1 được xây dựng.
  2. Foo f1 được chuyển đến work, trong đó có giá trị là f2.
  3. Hàm khởi động di chuyển của Foo là được gọi, di chuyển tất cả các tài nguyên trong f1 đến f2.
  4. Hiện tại, của trình phá hủy được gọi là, phát hành tất cả các tài nguyên.
  5. Bây giờ f1 của destructor được gọi là, mà không thực sự làm bất cứ điều gì vì tất cả các nguồn lực đã được chuyển giao đến f2. Tuy nhiên, destructor là gọi là dù sao.

Nhưng vì chỉ có một destructor được gọi là, bước 4 hoặc bước 5 không xảy ra. Tôi đã làm một backtrace từ destructor để xem nơi nó đã được invoked từ, và nó đang được gọi từ bước 5. Vậy tại sao không phải là f2 's destructor cũng được gọi là?

EDIT: OK, tôi đã sửa đổi điều này để nó thực sự quản lý tài nguyên. (Một bộ nhớ đệm nội bộ.) Tuy nhiên, tôi nhận được cùng một hành vi mà destructor chỉ được gọi một lần.

+0

trình biên dịch nào bạn đang thử nghiệm? – jalf

+0

gcc 4.3 ......................... – Channel72

+2

Trong trường hợp đó, có thể đáng lưu ý rằng nó được viết dựa trên phiên bản cũ hơn nhiều của C++ 0x dự thảo. Và di chuyển ngữ nghĩa đã được thay đổi khá nhiều kể từ đó. Ví dụ, tôi tin rằng một trình biên dịch mới hơn sẽ từ chối mã của bạn hoàn toàn trước khi bạn thêm 'std :: move'. Nó sẽ không thể gọi 'công việc' bởi vì đối số là một tham chiếu lvalue hiệu quả bởi vì nó được đặt tên. – jalf

Trả lời

9

Sửa(New và câu trả lời đúng)
Xin lỗi, nhìn sâu hơn về các mã, có vẻ như câu trả lời là đơn giản hơn nhiều: bạn không bao giờ gọi các nhà xây dựng di chuyển. Bạn không bao giờ thực sự di chuyển đối tượng. Bạn chỉ cần chuyển tham chiếu rvalue cho hàm work, gọi hàm thành viên trên tham chiếu, mà vẫn trỏ ngược lại đối tượng ban đầu.

câu trả lời gốc, lưu lại cho hậu thế

Để thực sự thực hiện di chuyển, bạn cần phải có một cái gì đó giống như Foo f3(std::move(f2)); bên work. Sau đó, bạn có thể gọi hàm thành viên của mình trên f3 là một đối tượng mới, được tạo bằng cách di chuyển từ f

Theo tôi thấy, bạn hoàn toàn không nhận được ngữ nghĩa di chuyển. Bạn chỉ nhìn thấy sự cắt bỏ bản sao cũ.

để di chuyển xảy ra, bạn phải sử dụng std::move (hoặc cụ thể, đối số được truyền cho hàm tạo phải là tham chiếu rvalue không được đặt tên/tạm thời, chẳng hạn như tham chiếu được trả lại từ std::move). Nếu không, nó được coi là một tham chiếu lvalue cũ, và sau đó một bản sao nên xảy ra, nhưng như thường lệ, trình biên dịch được phép tối ưu hóa nó đi, để lại cho bạn một đối tượng đang được xây dựng và một đối tượng bị hủy.

Dù sao, ngay cả với ngữ nghĩa di chuyển, không có lý do gì khiến trình biên dịch không nên làm điều tương tự: chỉ cần tối ưu hóa di chuyển, giống như nó đã tối ưu hóa bản sao. Một động thái là rẻ, nhưng nó vẫn còn rẻ hơn để chỉ xây dựng các đối tượng mà bạn cần nó, chứ không phải là xây dựng một, và sau đó di chuyển nó vào một vị trí khác và gọi destructor trên đầu tiên.

Cũng đáng lưu ý rằng bạn đang sử dụng trình biên dịch tương đối cũ và các phiên bản trước của thông số kỹ thuật không rõ ràng về những gì sẽ xảy ra với "đối tượng zombie" này. Vì vậy, có thể là GCC 4.3 không gọi là destructor. Tôi tin rằng đó chỉ là bản sửa đổi cuối cùng, hoặc có thể là bản sửa đổi trước đó, yêu cầu rõ ràng trình hủy được gọi là

+0

Được rồi, tôi đã thêm một lệnh gọi rõ ràng vào 'std :: move', chỉ để chắc chắn tôi thực sự chuyển một giá trị. Nhưng nó không thay đổi hành vi. – Channel72

+0

Tôi không mong đợi nó. Theo như tôi biết, trình biên dịch được phép thực hiện sao chép (hoặc di chuyển elision, tôi đoán) về tham chiếu rvalue: trong trường hợp thử nghiệm đơn giản của bạn, nó có thể xây dựng đối tượng tại chỗ, thay vì xây dựng một và sau đó di chuyển nó. – jalf

+0

Hmm ... được rồi, tôi đã chỉnh sửa mã để 'Foo' hiện thực sự quản lý tài nguyên. Nó phân bổ một bộ đệm và chuyển quyền sở hữu của bộ đệm trong hàm khởi tạo. Tuy nhiên, destructor vẫn chỉ được gọi một lần. Tôi có làm gì sai ở đây không? – Channel72

2

Xin lưu ý rằng trình biên dịch có thể tối ưu hóa việc xây dựng/hủy không cần thiết, chỉ làm cho một đối tượng tồn tại một cách hiệu quả. Điều này đặc biệt đúng đối với các tham chiếu rvalue (được phát minh chính xác cho mục đích này).

Tôi nghĩ bạn hiểu sai về những gì xảy ra. Hàm khởi tạo di chuyển không được gọi: nếu giá trị không phải là tạm thời, tham chiếu rvalue hoạt động như một tham chiếu bình thường.

Có thể this article sẽ mang lại nhiều thông tin hơn về ngữ nghĩa tham chiếu rvalue.

+0

Thậm chí nếu tôi không biên dịch với bất kỳ cờ tối ưu nào? – Channel72

+3

@ Channel72: tiêu chuẩn C++ không quan tâm đến cờ tối ưu hóa. Một sự tối ưu hóa hoặc là hợp pháp (nó tôn trọng ngữ nghĩa của C++), hoặc nó không phải là. Và miễn là nó là hợp pháp, trình biên dịch có thể áp dụng nó bất cứ khi nào nó thích, theo như tiêu chuẩn C++ có liên quan. Tất nhiên, nếu nó không hợp pháp, nó sẽ * không bao giờ * được áp dụng.Trình biên dịch thường thực hiện một số tối ưu hóa nhỏ (RVO, ví dụ, và trên một số trình biên dịch, NRVO là tốt) ngay cả khi tối ưu hóa bị vô hiệu hóa. Nó cũng có thể thực hiện sao chép elision, mà sẽ giải thích kết quả của bạn – jalf

+1

Tôi nghĩ rằng không có sao chép ở tất cả, vì bạn không thực sự có bất kỳ đối tượng tạm thời trong mã của bạn. – Vlad

Các vấn đề liên quan