Tại sao hàm hủy được thực hiện hai lần?


12
#include <iostream>
using namespace std;

class Car
{
public:
    ~Car()  { cout << "Car is destructed." << endl; }
};

class Taxi :public Car
{
public:
    ~Taxi() {cout << "Taxi is destructed." << endl; }
};

void test(Car c) {}

int main()
{
    Taxi taxi;
    test(taxi);
    return 0;
}

đây là đầu ra :

Car is destructed.
Car is destructed.
Taxi is destructed.
Car is destructed.

Tôi sử dụng MS Visual Studio Community 2017 (Xin lỗi, tôi không biết cách xem phiên bản của Visual C ++). Khi tôi sử dụng chế độ gỡ lỗi. Tôi tìm thấy một hàm hủy được thực thi khi rời khỏi void test(Car c){ }thân hàm như mong đợi. Và một kẻ hủy diệt thêm xuất hiện khi kết test(taxi);thúc.

Các test(Car c)chức năng sử dụng giá trị như tham số chính thức. Một chiếc xe được sao chép khi đi đến chức năng. Vì vậy, tôi nghĩ rằng sẽ chỉ có một "Xe bị phá hủy" khi rời khỏi chức năng. Nhưng thực tế có hai "Xe bị phá hủy" khi rời khỏi chức năng. (Dòng thứ nhất và thứ hai như được hiển thị trong đầu ra) Tại sao có hai "Xe bị phá hủy"? Cảm ơn bạn.

===============

khi tôi thêm một hàm ảo class Car chẳng hạn: virtual void drive() {} Sau đó tôi nhận được đầu ra mong đợi.

Car is destructed.
Taxi is destructed.
Car is destructed.

3
Có thể là một vấn đề trong cách trình biên dịch xử lý việc cắt đối tượng khi chuyển một Taxiđối tượng đến một hàm lấy một Carđối tượng theo giá trị?
Một số lập trình viên anh chàng

1
Phải là trình biên dịch C ++ cũ của bạn. g ++ 9 cho kết quả như mong đợi. Sử dụng trình gỡ lỗi để xác định lý do tại sao một bản sao bổ sung của đối tượng được tạo ra.
Sam Varshavchik

2
Tôi đã thử nghiệm g ++ với phiên bản 7.4.0 và clang ++ với phiên bản 6.0.0. Họ đã cho đầu ra dự kiến ​​khác với đầu ra của op. Vì vậy, vấn đề có thể là về trình biên dịch mà anh ta sử dụng.
Marceline

1
Tôi đã sao chép bằng MS Visual C ++. Nếu tôi thêm một hàm tạo sao chép do người dùng định nghĩa và hàm tạo mặc định Carthì vấn đề này sẽ biến mất và nó mang lại kết quả như mong đợi.
interjay

1
Vui lòng thêm trình biên dịch và phiên bản cho câu hỏi
Các cuộc đua Lightness trong Orbit

Câu trả lời:


7

Có vẻ như trình biên dịch Visual Studio đang sử dụng một phím tắt khi cắt taxicuộc gọi hàm của bạn , điều này thật trớ trêu khi nó thực hiện nhiều công việc hơn người ta tưởng.

Đầu tiên, nó lấy của bạn taxivà sao chép cấu trúc Cartừ nó, để đối số khớp.

Sau đó, nó sao chép Car lại một lần nữa cho giá trị truyền qua.

Hành vi này biến mất khi bạn thêm một hàm tạo sao chép do người dùng định nghĩa, do đó trình biên dịch dường như thực hiện điều này vì lý do riêng của nó (có lẽ, bên trong, đó là một đường dẫn mã đơn giản hơn), sử dụng thực tế là nó được "cho phép" vì sao chép chính nó là tầm thường. Thực tế là bạn vẫn có thể quan sát hành vi này bằng cách sử dụng một hàm hủy không tầm thường là một chút quang sai.

Tôi không biết mức độ hợp pháp của nó (đặc biệt là từ C ++ 17) hoặc tại sao trình biên dịch sẽ sử dụng phương pháp này, nhưng tôi đồng ý rằng đó không phải là đầu ra mà tôi mong đợi bằng trực giác. Cả GCC và Clang đều không làm điều này, mặc dù có thể họ làm mọi thứ theo cùng một cách nhưng sau đó tốt hơn trong việc trốn tránh bản sao. Tôi đã nhận thấy rằng ngay cả VS 2019 vẫn không tuyệt vời trong cuộc bầu cử được đảm bảo.


Xin lỗi, nhưng đây không phải là chính xác những gì tôi đã nói với "chuyển đổi từ Taxi thành Xe hơi nếu trình biên dịch của bạn không thực hiện việc sao chép."
Christophe

Đó là một nhận xét không công bằng, bởi vì vượt qua giá trị so với vượt qua bằng cách từ chối để tránh cắt chỉ được thêm vào trong một chỉnh sửa, để giúp OP vượt qua câu hỏi này. Sau đó, câu trả lời của tôi không phải là một phát súng trong bóng tối, nó đã được giải thích rõ ràng từ đầu nó có thể đến từ đâu và tôi rất vui khi thấy bạn đi đến kết luận tương tự. Bây giờ nhìn vào công thức của bạn, "Có vẻ như ... tôi không biết", tôi nghĩ rằng có một số lượng không chắc chắn ở đây, bởi vì thật lòng tôi cũng không hiểu tại sao trình biên dịch cần tạo ra temp này.
Christophe

Được rồi, sau đó loại bỏ các phần không liên quan đến câu trả lời của bạn, chỉ để lại một đoạn liên quan duy nhất phía sau
Các cuộc đua Lightness trong Orbit

Ok, tôi đã loại bỏ đoạn cắt xén gây mất tập trung và tôi đã biện minh cho quan điểm về việc sao chép bản sao với các tham chiếu chính xác đến tiêu chuẩn.
Christophe

Bạn có thể giải thích lý do tại sao một chiếc xe tạm thời nên được xây dựng sao chép từ Taxi và sau đó sao chép lại vào tham số không? Và tại sao trình biên dịch không làm điều này khi được cung cấp với một chiếc xe đơn giản?
Christophe

3

Chuyện gì đang xảy ra vậy?

Khi bạn tạo một Taxi, bạn cũng tạo một tiểu dự án Car. Và khi chiếc taxi bị phá hủy, cả hai đối tượng đều bị phá hủy. Khi bạn gọi test()bạn vượt qua Cargiá trị. Vì vậy, một giây Carđược xây dựng sao chép và sẽ bị phá hủy khi test()còn lại. Vì vậy, chúng tôi có một lời giải thích cho 3 hàm hủy: đầu tiên và hai cuối cùng trong chuỗi.

Hàm hủy thứ tư (đó là thứ hai trong chuỗi) là bất ngờ và tôi không thể sao chép với các trình biên dịch khác.

Nó chỉ có thể là tạm thời Carđược tạo như là nguồn cho Carđối số. Vì nó không xảy ra khi cung cấp trực tiếp một Cargiá trị làm đối số, tôi nghi ngờ đó là để chuyển đổi Taxithành Car. Điều này là bất ngờ, vì đã có một tiểu dự án Cartrong mỗi Taxi. Do đó, tôi nghĩ rằng trình biên dịch thực hiện một chuyển đổi không cần thiết thành một temp và không thực hiện các cuộc bầu chọn sao chép có thể tránh được temp này.

Làm rõ trong các ý kiến:

Ở đây làm rõ với tham chiếu đến tiêu chuẩn cho luật sư ngôn ngữ để xác minh khiếu nại của tôi:

  • Chuyển đổi mà tôi đang đề cập ở đây, là một chuyển đổi của nhà xây dựng [class.conv.ctor], tức là xây dựng một đối tượng của một lớp (ở đây là Xe hơi) dựa trên một đối số của loại khác (ở đây là Taxi).
  • Chuyển đổi này sử dụng sau đó một đối tượng tạm thời để trả về Cargiá trị của nó . Trình biên dịch sẽ được phép thực hiện một cuộc bầu chọn sao chép [class.copy.elision]/1.1, vì thay vì xây dựng tạm thời, nó có thể xây dựng giá trị được trả trực tiếp vào tham số.
  • Vì vậy, nếu temp này mang lại hiệu ứng phụ, thì đó là do trình biên dịch dường như không sử dụng phương pháp sao chép có thể này. Điều đó không sai, vì việc sao chép bản sao là không bắt buộc.

Xác nhận thí nghiệm của anaysis

Bây giờ tôi có thể tái tạo trường hợp của bạn bằng cách sử dụng cùng một trình biên dịch và vẽ một thử nghiệm để xác nhận những gì đang xảy ra.

Giả định của tôi ở trên là trình biên dịch đã chọn một quá trình truyền tham số dưới mức tối ưu, sử dụng chuyển đổi hàm tạo Car(const &Taxi)thay vì sao chép cấu trúc trực tiếp từ tiểu dự án Carcủa Taxi.

Vì vậy, tôi đã cố gắng gọi test()nhưng rõ ràng đúc Taxithành một Car.

Nỗ lực đầu tiên của tôi đã không thành công để cải thiện tình hình. Trình biên dịch vẫn sử dụng chuyển đổi hàm tạo tối ưu:

test(static_cast<Car>(taxi));  // produces the same result with 4 destructor messages

Nỗ lực thứ hai của tôi đã thành công. Nó cũng thực hiện quá trình truyền, nhưng sử dụng đúc con trỏ để đề xuất mạnh mẽ trình biên dịch sử dụng tiểu dự án Carcủa Taxivà không tạo đối tượng tạm thời ngớ ngẩn này:

test(*static_cast<Car*>(&taxi));  //  :-)

Và thật bất ngờ: nó hoạt động như mong đợi, chỉ tạo ra 3 thông báo hủy :-)

Kết thúc thí nghiệm:

Trong một thử nghiệm cuối cùng, tôi đã cung cấp một hàm tạo tùy chỉnh bằng cách chuyển đổi:

 class Car {
 ... 
     Car(const Taxi& t);  // not necessary but for experimental purpose
 }; 

và thực hiện nó với *this = *static_cast<Car*>(&taxi);. Nghe có vẻ ngớ ngẩn, nhưng điều này cũng tạo ra mã sẽ chỉ hiển thị 3 thông báo hủy, do đó tránh các đối tượng tạm thời không cần thiết.

Điều này dẫn đến việc nghĩ rằng có thể có một lỗi trong trình biên dịch gây ra hành vi này. Đó là khả năng sao chép trực tiếp - việc xây dựng từ lớp cơ sở sẽ bị bỏ qua trong một số trường hợp.


2
Không trả lời câu hỏi
Các cuộc đua nhẹ nhàng trong quỹ đạo

1
@qiazi Tôi nghĩ rằng điều này xác nhận giả thuyết tạm thời cho việc chuyển đổi mà không cần bỏ qua bản sao, bởi vì tạm thời này sẽ được tạo ra khỏi chức năng, trong ngữ cảnh của trình gọi.
Christophe

1
Khi nói "việc chuyển đổi từ Taxi thành Xe hơi nếu trình biên dịch của bạn không thực hiện cuộc bầu chọn sao chép", bạn đang đề cập đến cuộc bầu cử sao chép nào? Không nên có bản sao cần được tách ra ngay từ đầu.
interjay

1
@interjay vì trình biên dịch không cần xây dựng Xe tạm thời dựa trên đối tượng phụ Xe của Taxi để thực hiện chuyển đổi và sau đó sao chép temp này vào tham số Car: nó có thể tách bản sao và trực tiếp xây dựng tham số từ phụ đề gốc.
Christophe

1
Sao chép bản sao là khi các tiêu chuẩn tuyên bố rằng một bản sao sẽ được tạo ra, nhưng trong một số trường hợp nhất định cho phép bản sao được tách ra. Trong trường hợp này, không có lý do nào để tạo một bản sao ở vị trí đầu tiên (một tham chiếu Taxicó thể được chuyển trực tiếp đến hàm tạo Carsao chép), vì vậy việc sao chép bản sao là không liên quan.
interjay
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.