Mengapa destruktor dieksekusi dua kali?

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

ini adalah output :

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

Saya menggunakan MS Visual Studio Community 2017 (Maaf, saya tidak tahu cara melihat edisi Visual C ++). Ketika saya menggunakan mode debug. Saya menemukan satu destruktor dijalankan ketika meninggalkan void test(Car c){ }fungsi tubuh seperti yang diharapkan. Dan destruktor tambahan muncul ketika test(taxi);selesai.

The test(Car c)fungsi menggunakan nilai sebagai parameter formal. Mobil disalin ketika pergi ke fungsi. Jadi saya pikir hanya akan ada satu "Mobil hancur" ketika meninggalkan fungsi. Tetapi sebenarnya ada dua "Mobil hancur" ketika meninggalkan fungsi. (Baris pertama dan kedua seperti yang ditunjukkan dalam output) Mengapa ada dua "Mobil rusak"? Terima kasih.

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

ketika saya menambahkan fungsi virtual class Car misalnya: virtual void drive() {} Lalu saya mendapatkan output yang diharapkan.

Car is destructed.
Taxi is destructed.
Car is destructed.
qiazi
sumber
3
Bisa menjadi masalah dalam bagaimana kompilator menangani objek mengiris ketika meneruskan Taxiobjek ke fungsi yang mengambil Carobjek dengan nilai?
Beberapa programmer Bung
1
Harus menjadi kompiler C ++ lama Anda. g ++ 9 memberikan hasil yang diharapkan. Gunakan debugger untuk menentukan alasan mengapa salinan tambahan objek dibuat.
Sam Varshavchik
2
Saya telah menguji g ++ dengan versi 7.4.0 dan dentang ++ dengan versi 6.0.0. Mereka memberikan output yang diharapkan yang berbeda dari output op. Jadi masalahnya mungkin tentang kompiler yang dia gunakan.
Marceline
1
Saya direproduksi dengan MS Visual C ++. Jika saya menambahkan copy-constructor yang ditentukan pengguna dan konstruktor default untuk Carkemudian masalah ini menghilang dan memberikan hasil yang diharapkan.
interjay
1
Silakan tambahkan kompiler dan versi untuk pertanyaan
Lightness Races di Orbit

Jawaban:

7

Sepertinya kompiler Visual Studio mengambil sedikit jalan pintas ketika mengiris Anda taxiuntuk pemanggilan fungsi, yang ironisnya menghasilkan itu melakukan lebih banyak pekerjaan daripada yang mungkin diharapkan.

Pertama, ini mengambil Anda taxidan menyalin-membangun Cardari itu, sehingga argumen cocok.

Kemudian, itu menyalin Car lagi untuk nilai pass-by-value.

Perilaku ini hilang ketika Anda menambahkan copy constructor yang ditentukan pengguna, sehingga kompiler tampaknya melakukan ini karena alasannya sendiri (mungkin, secara internal, ini adalah jalur kode yang lebih sederhana), menggunakan fakta bahwa itu "diizinkan" karena salin itu sendiri sepele. Fakta bahwa Anda masih dapat mengamati perilaku ini menggunakan destructor non-sepele adalah sedikit penyimpangan.

Saya tidak tahu sejauh mana ini legal (terutama sejak C ++ 17), atau mengapa kompiler akan mengambil pendekatan ini, tetapi saya akan setuju bahwa itu bukan output yang saya harapkan secara intuitif diharapkan. Baik GCC atau Dentang melakukan ini, meskipun mungkin mereka melakukan hal-hal dengan cara yang sama tetapi kemudian lebih baik dalam menghilangkan salinan. Saya telah memperhatikan bahwa bahkan VS 2019 masih tidak hebat dalam jaminan elisi.

Lightness Races di Orbit
sumber
Maaf, tapi bukankah ini tepatnya yang saya katakan dengan "konversi dari Taksi ke Mobil jika kompiler Anda tidak melakukan copy elision."
Christophe
Itu adalah komentar yang tidak adil, karena pass by value vs pass byece untuk menghindari slicing hanya ditambahkan dalam edit, untuk membantu OP melampaui pertanyaan ini. Maka jawaban saya bukanlah suntikan dalam gelap, itu jelas dijelaskan dari awal dari mana itu berasal dan saya senang melihat bahwa Anda sampai pada kesimpulan yang sama. Sekarang melihat formulasi Anda, "Sepertinya ... saya tidak tahu", saya pikir ada jumlah ketidakpastian yang sama di sini, karena terus terang saya maupun Anda tidak mengerti mengapa kompiler perlu membuat temp ini.
Christophe
Oke lalu hapus bagian yang tidak terkait dari jawaban Anda dengan hanya menyisakan satu paragraf terkait di belakang
Lightness Races in Orbit
Oke, saya menghapus para slicing slicing yang mengganggu, dan saya sudah membenarkan poin tentang copy elision dengan referensi yang tepat ke standar.
Christophe
Bisakah Anda menjelaskan mengapa mobil sementara harus dibuat dari Taksi dan kemudian disalin lagi ke dalam parameter? Dan mengapa kompiler tidak melakukan ini ketika disediakan dengan mobil biasa?
Christophe
3

Apa yang terjadi ?

Saat Anda membuat Taxi, Anda juga membuat Carsubobjek. Dan ketika taksi dihancurkan, kedua benda hancur. Ketika Anda menelepon, test()Anda melewati nilai Cardengan. Jadi yang kedua Carakan dikopi dan akan hancur ketika test()dibiarkan. Jadi kami memiliki penjelasan untuk 3 destruktor: yang pertama dan yang kedua terakhir secara berurutan.

Destructor keempat (yang kedua dalam urutan) tidak terduga dan saya tidak bisa mereproduksi dengan kompiler lain.

Itu hanya bisa Cardibuat sementara sebagai sumber untuk Carargumen. Karena itu tidak terjadi ketika memberikan langsung Carnilai sebagai argumen, saya menduga itu untuk mengubah ke Taxidalam Car. Ini tidak terduga, karena sudah ada Carsub proyek di setiap Taxi. Oleh karena itu saya berpikir bahwa kompiler melakukan konversi yang tidak perlu menjadi temp dan tidak melakukan copy elision yang bisa menghindari temp ini.

Klarifikasi yang diberikan dalam komentar:

Di sini klarifikasi dengan mengacu pada standar pengacara bahasa untuk memverifikasi klaim saya:

  • Konversi yang saya maksudkan di sini, adalah konversi oleh konstruktor [class.conv.ctor], yaitu membangun objek dari satu kelas (di sini Mobil) berdasarkan argumen dari tipe lain (di sini Taksi).
  • Konversi ini menggunakan objek sementara untuk mengembalikan Carnilainya. Kompiler akan diizinkan untuk membuat salinan salinan sesuai [class.copy.elision]/1.1, karena alih-alih membangun sementara, itu dapat membangun nilai yang akan dikembalikan langsung ke parameter.
  • Jadi jika temp ini memberikan efek samping, itu karena kompiler tampaknya tidak menggunakan kemungkinan copy-elision ini. Itu tidak salah, karena salinan elision tidak wajib.

Konfirmasi eksperimental anaysis

Saya sekarang dapat mereproduksi kasing Anda dengan menggunakan kompiler yang sama dan menggambar percobaan untuk mengonfirmasi apa yang sedang terjadi.

Asumsi saya di atas adalah bahwa kompiler memilih proses melewati parameter suboptimal, menggunakan konversi konstruktor Car(const &Taxi)alih-alih menyalin konstruksi langsung dari Carsubobjek dari Taxi.

Jadi saya mencoba menelepon test()tetapi secara eksplisit melemparkan Taxike dalam Car.

Upaya pertama saya tidak berhasil memperbaiki situasi. Kompiler masih menggunakan konversi konstruktor suboptimal:

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

Upaya kedua saya berhasil. Itu melakukan casting juga, tetapi menggunakan pointer casting agar sangat menyarankan kompiler untuk menggunakan Carsubobjek dari Taxidan tanpa membuat objek sementara konyol ini:

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

Dan mengejutkan: berfungsi seperti yang diharapkan, hanya menghasilkan 3 pesan penghancuran :-)

Eksperimen penutup:

Dalam percobaan terakhir, saya memberikan konstruktor khusus dengan konversi:

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

dan mengimplementasikannya dengan *this = *static_cast<Car*>(&taxi);. Kedengarannya konyol, tetapi ini juga menghasilkan kode yang hanya akan menampilkan 3 pesan destruktor, sehingga menghindari objek sementara yang tidak perlu.

Ini membuat kami berpikir bahwa mungkin ada bug di kompiler yang menyebabkan perilaku ini. Ini adalah kemungkinan salinan-konstruksi langsung dari kelas dasar akan terjawab dalam beberapa keadaan.

Christophe
sumber
2
Tidak menjawab pertanyaan
Lightness Races di Orbit
1
@ qiazi Saya pikir ini mengkonfirmasi hipotesis sementara untuk konversi tanpa salinan elisi, karena sementara ini akan dihasilkan dari fungsi, dalam konteks pemanggil.
Christophe
1
Ketika mengatakan "konversi dari Taksi ke Mobil jika kompiler Anda tidak melakukan copy elision", salinan elision apa yang Anda maksud? Seharusnya tidak ada salinan yang perlu dielakkan terlebih dahulu.
interjay
1
@interjay karena kompiler tidak perlu membuat mobil sementara berdasarkan sub-objek Mobil Taksi untuk melakukan konversi dan kemudian menyalin temp ini ke dalam parameter Car: ia bisa menghilangkan salinan dan langsung membangun parameter dari subobject asli.
Christophe
1
Salinan salinan adalah ketika standar menyatakan bahwa salinan harus dibuat, tetapi dalam keadaan tertentu memungkinkan salinan untuk dihilangkan. Dalam hal ini, tidak ada alasan untuk salinan yang akan dibuat di tempat pertama (referensi untuk Taxidapat diteruskan langsung ke Carcopy constructor), sehingga salinan salinan tidak relevan.
interjay