เหตุใด destructor จึงถูกดำเนินการสองครั้ง


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

นี่คือผลลัพธ์ :

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

ฉันใช้ MS Visual Studio Community 2017 (ขออภัยฉันไม่ทราบวิธีดูรุ่น Visual C ++) เมื่อฉันใช้โหมดดีบัก ฉันพบว่ามี destructor หนึ่งตัวเมื่อออกจากvoid test(Car c){ }ฟังก์ชันตามที่คาดไว้ และdestructor พิเศษปรากฏขึ้นเมื่อtest(taxi);มีมากกว่า

test(Car c)คุ้มค่าการใช้งานฟังก์ชั่นเป็นพารามิเตอร์ที่เป็นทางการ รถยนต์จะถูกคัดลอกเมื่อไปที่ฟังก์ชั่น ดังนั้นฉันคิดว่าจะมีเพียง "รถยนต์ถูกทำลาย" เมื่อออกจากฟังก์ชั่น แต่จริงๆแล้วมี "รถยนต์ถูกทำลาย" สองอันเมื่อออกจากฟังก์ชั่น (บรรทัดแรกและบรรทัดที่สองตามที่แสดงในเอาต์พุต) ทำไมจึงมี "รถยนต์ถูกทำลาย" สองคัน ขอบคุณ.

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

เมื่อฉันเพิ่มฟังก์ชั่นเสมือนในclass Car ตัวอย่าง: virtual void drive() {} จากนั้นฉันก็จะได้ผลลัพธ์ที่คาดหวัง

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

3
อาจเป็นปัญหาว่าคอมไพเลอร์จัดการกับการแบ่งวัตถุอย่างไรเมื่อส่งTaxiวัตถุไปยังฟังก์ชันที่รับค่าCarวัตถุ?
โปรแกรมเมอร์บางคนเพื่อน

1
ต้องเป็นคอมไพเลอร์ C ++ เก่าของคุณ g ++ 9 ให้ผลลัพธ์ที่คาดหวัง ใช้ดีบักเกอร์เพื่อระบุสาเหตุที่ทำให้มีการทำสำเนาวัตถุเพิ่ม
Sam Varshavchik

2
ฉันได้ทดสอบ g ++ กับเวอร์ชัน 7.4.0 และ clang ++ กับเวอร์ชัน 6.0.0 พวกเขาให้ผลลัพธ์ที่คาดหวังซึ่งแตกต่างจากผลลัพธ์ของ op ดังนั้นปัญหาอาจจะเกี่ยวกับคอมไพเลอร์ที่เขาใช้
Marceline

1
ฉันทำซ้ำด้วย MS Visual C ++ ถ้าฉันเพิ่มตัวคัดลอกคอนสตรัคเตอร์ที่ผู้ใช้กำหนดและคอนสตรัคเตอร์เริ่มต้นสำหรับCarปัญหานี้จะหายไปและให้ผลลัพธ์ที่คาดหวัง
ระหว่าง

1
โปรดเพิ่มคอมไพเลอร์และรุ่นให้กับคำถาม
Lightness Races ใน Orbit

คำตอบ:


7

ดูเหมือนคอมไพเลอร์ Visual Studio ใช้เวลาลัดเล็กน้อยเมื่อแบ่งส่วนของคุณtaxiสำหรับการเรียกใช้ฟังก์ชันซึ่งส่งผลให้มันทำงานได้มากกว่าที่คาดไว้

ขั้นแรกให้คุณtaxiคัดลอกและคัดลอกCarจากเพื่อให้ข้อโต้แย้งตรงกัน

จากนั้นก็คัดลอกCar อีกครั้งสำหรับการส่งผ่านค่า

พฤติกรรมนี้หายไปเมื่อคุณเพิ่มตัวสร้างสำเนาที่ผู้ใช้กำหนดเองดังนั้นคอมไพเลอร์ดูเหมือนว่าจะทำสิ่งนี้ด้วยเหตุผลของตัวเอง (บางทีภายในเป็นเส้นทางรหัสที่ง่ายกว่า) โดยใช้ข้อเท็จจริงที่ว่า "อนุญาต" เนื่องจาก คัดลอกตัวเองเป็นเรื่องเล็กน้อย ความจริงที่ว่าคุณยังสามารถสังเกตเห็นพฤติกรรมนี้โดยใช้ตัวทำลายที่ไม่สำคัญคือความผิดปกติเล็กน้อย

ฉันไม่ทราบว่าสิ่งนี้ถูกต้องตามกฎหมาย (โดยเฉพาะอย่างยิ่งตั้งแต่ C ++ 17) หรือทำไมคอมไพเลอร์จะใช้วิธีการนี้ แต่ฉันยอมรับว่ามันไม่ใช่ผลลัพธ์ที่ฉันคาดหวังอย่างสังหรณ์ใจ ทั้ง GCC และ Clang ไม่ทำสิ่งนี้แม้ว่าอาจเป็นไปได้ว่าพวกเขาทำสิ่งต่าง ๆ ด้วยวิธีเดียวกัน ฉันได้สังเกตเห็นว่าแม้ VS 2019 ยังไม่ยอดเยี่ยมเท่าที่รับประกัน


ขออภัยนี่ไม่ใช่สิ่งที่ฉันพูดด้วย "การแปลงจากแท็กซี่เป็นรถยนต์หากคอมไพเลอร์ของคุณไม่ทำสำเนาคัดลอก"
Christophe

นั่นเป็นคำพูดที่ไม่ยุติธรรมเพราะการส่งผ่านตามค่าเทียบกับการส่งผ่านโดยการอ้างอิงเพื่อหลีกเลี่ยงการแบ่งเป็นเพียงการแก้ไขเพื่อช่วย OP นอกเหนือจากคำถามนี้ จากนั้นคำตอบของฉันไม่ได้ถูกยิงในที่มืดมันมีคำอธิบายอย่างชัดเจนตั้งแต่เริ่มต้นว่ามันอาจมาจากไหนและฉันดีใจที่เห็นว่าคุณได้ข้อสรุปเดียวกัน ตอนนี้ดูสูตรของคุณ "ดูเหมือนว่า ... ฉันไม่รู้" ฉันคิดว่ามีความไม่แน่นอนอยู่ที่นี่เพราะฉันและคุณไม่เข้าใจว่าทำไมคอมไพเลอร์จึงต้องสร้างอุณหภูมินี้
Christophe

เอาล่ะเอาส่วนที่ไม่เกี่ยวข้องของคำตอบของคุณออกเพียงย่อหน้าเดียวที่เกี่ยวข้องหลัง
Lightness Races ใน Orbit

ตกลงฉันลบพาราแบ่งส่วนเบี่ยงเบนความสนใจออกและฉันได้พิสูจน์จุดที่เกี่ยวกับการคัดลอกข้อมูลด้วยการอ้างอิงที่แม่นยำกับมาตรฐาน
Christophe

คุณช่วยอธิบายได้ไหมว่าทำไมรถยนต์ชั่วคราวควรคัดลอกมาจากแท็กซี่แล้วคัดลอกอีกครั้งลงในพารามิเตอร์ และทำไมคอมไพเลอร์ไม่ทำเช่นนี้เมื่อมีรถธรรมดา?
Christophe

3

เกิดอะไรขึ้น ?

เมื่อคุณสร้าง a Taxiคุณจะสร้างCarsubobject และเมื่อแท็กซี่ถูกทำลายวัตถุทั้งสองจะถูกทำลาย เมื่อคุณโทรหาtest()คุณผ่านCarค่า ดังนั้นวินาทีCarจะได้รับการคัดลอกและจะถูกทำลายเมื่อtest()เหลือ ดังนั้นเราจึงมีคำอธิบายสำหรับนักทำลาย 3 คน: ลำดับแรกและสองคนสุดท้ายในลำดับ

destructor ที่สี่ (นั่นคือลำดับที่สองในลำดับ) ไม่คาดคิดและฉันไม่สามารถทำซ้ำกับคอมไพเลอร์อื่น ๆ ได้

มันสามารถCarสร้างขึ้นชั่วคราวเป็นแหล่งที่มาสำหรับการCarโต้แย้ง เพราะมันไม่ได้เกิดขึ้นเมื่อให้โดยตรงCarค่าเป็นอาร์กิวเมนต์ฉันสงสัยว่าจะเป็นสำหรับการเปลี่ยนเข้าสู่Taxi Carนี่เป็นสิ่งที่คาดไม่ถึงเนื่องจากมีCarsubobject อยู่แล้วในทุกTaxiๆ ดังนั้นฉันคิดว่าคอมไพเลอร์ทำการแปลงที่ไม่จำเป็นลงในอุณหภูมิและไม่ทำการคัดลอกตัวเลือกที่สามารถหลีกเลี่ยงอุณหภูมินี้ได้

ชี้แจงให้ชัดเจนในความคิดเห็น:

นี่คือคำชี้แจงที่มีการอ้างอิงถึงมาตรฐานสำหรับภาษาทนายความเพื่อตรวจสอบการเรียกร้องของฉัน:

  • การแปลงที่ฉันอ้างถึงที่นี่คือการแปลงโดยคอนสตรัค[class.conv.ctor]เตอร์คือการสร้างวัตถุของคลาสหนึ่ง (รถยนต์ที่นี่) ตามอาร์กิวเมนต์ประเภทอื่น (ที่นี่แท็กซี่)
  • การแปลงนี้ใช้วัตถุชั่วคราวเพื่อคืนCarค่า คอมไพเลอร์จะได้รับอนุญาตให้ทำการคัดลอกการคัดลอกตาม[class.copy.elision]/1.1เพราะแทนที่จะสร้างชั่วคราวก็สามารถสร้างค่าที่จะส่งกลับโดยตรงในพารามิเตอร์
  • ดังนั้นหากอุณหภูมินี้ให้ผลข้างเคียงก็เป็นเพราะคอมไพเลอร์ไม่ได้ใช้ประโยชน์จากการคัดลอกที่เป็นไปได้นี้ มันไม่ถูกต้องเนื่องจากการคัดลอกจะไม่บังคับ

การยืนยันการทดลองของ anaysis

ตอนนี้ฉันสามารถทำซ้ำกรณีของคุณโดยใช้คอมไพเลอร์เดียวกันและวาดการทดสอบเพื่อยืนยันสิ่งที่เกิดขึ้น

สมมติฐานของฉันข้างต้นก็คือว่าคอมไพเลอร์ที่เลือกเป็นกระบวนการที่ก่อให้เกิดผลลัพธ์พารามิเตอร์ผ่านโดยใช้การแปลงคอนสตรัคCar(const &Taxi)แทนการคัดลอกก่อสร้างโดยตรงจากCarsubobject Taxiของ

ดังนั้นผมจึงพยายามเรียกtest()แต่อย่างชัดเจนหล่อเป็นTaxiCar

ความพยายามครั้งแรกของฉันไม่ประสบความสำเร็จในการปรับปรุงสถานการณ์ คอมไพเลอร์ยังคงใช้การแปลงคอนสตรัคที่ยอดเยี่ยม:

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

ความพยายามครั้งที่สองของฉันสำเร็จ มันจะทำการร่ายด้วยเช่นกัน แต่ใช้การชี้แบบชี้เพื่อแนะนำคอมไพเลอร์อย่างยิ่งให้ใช้Carsubobject ของTaxiและโดยไม่สร้างวัตถุชั่วคราวที่โง่เง่านี้:

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

และน่าประหลาดใจ: ใช้งานได้ตามที่คาดหวังมีเพียง 3 ข้อความทำลาย :-)

การสรุปการทดลอง:

ในการทดสอบขั้นสุดท้ายฉันได้ให้ตัวสร้างแบบกำหนดเองโดยการแปลง:

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

*this = *static_cast<Car*>(&taxi);และดำเนินการด้วย ฟังดูงี่เง่า แต่สิ่งนี้ก็สร้างรหัสที่จะแสดงเฉพาะข้อความ destructor 3 ข้อความเท่านั้นดังนั้นการหลีกเลี่ยงวัตถุชั่วคราวที่ไม่จำเป็น

สิ่งนี้นำไปสู่การคิดว่าอาจมีข้อผิดพลาดในคอมไพเลอร์ที่ทำให้เกิดพฤติกรรมนี้ มันอยู่ที่ความเป็นไปได้ของการคัดลอกโดยตรงจากคลาสฐานที่จะพลาดในบางสถานการณ์


2
ไม่ตอบคำถาม
Lightness Races ใน Orbit

1
@ qiazi ฉันคิดว่าสิ่งนี้เป็นการยืนยันสมมติฐานของการแปลงชั่วคราวโดยไม่มีการคัดลอกคัดลอกเพราะชั่วคราวนี้จะถูกสร้างขึ้นจากฟังก์ชั่นในบริบทของผู้โทร
Christophe

1
เมื่อพูดว่า "การแปลงจากรถแท็กซี่เป็นรถถ้าคอมไพเลอร์ของคุณไม่ทำสำเนาคัดลอก" คุณหมายถึงอะไรคัดลอกคัดลอก? ไม่ควรมีการคัดลอกซึ่งจะต้องมีการแก้ไขในครั้งแรก
ระหว่าง

1
@interjay เนื่องจากคอมไพเลอร์ไม่จำเป็นต้องสร้างรถยนต์ชั่วคราวโดยยึดตามวัตถุย่อยของรถแท็กซี่เพื่อทำการแปลงแล้วคัดลอก temp นี้ลงในพารามิเตอร์ Car: มันสามารถคัดลอกและสร้างพารามิเตอร์จาก subobject ดั้งเดิมได้โดยตรง
Christophe

1
การคัดลอกการคัดลอกคือเมื่อสถานะมาตรฐานที่ควรสร้างสำเนา แต่ภายใต้สถานการณ์บางอย่างอนุญาตให้คัดลอกการคัดลอก ในกรณีนี้ไม่มีเหตุผลสำหรับการคัดลอกที่จะสร้างขึ้นในสถานที่แรก (การอ้างอิงถึงTaxiสามารถส่งโดยตรงไปยังตัวCarสร้างการคัดลอก) ดังนั้นการคัดลอกการคัดลอกจะไม่เกี่ยวข้อง
ระหว่าง
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.