โยนข้อยกเว้นออกมาจากผู้ทำลาย


257

คนส่วนใหญ่บอกว่าจะไม่ส่งข้อยกเว้นใด ๆ จากผู้ทำลายล้างซึ่งส่งผลให้เกิดพฤติกรรมที่ไม่ได้กำหนด Stroustrup ทำให้จุดที่"เวกเตอร์ destructor อย่างชัดเจนเรียก destructor สำหรับทุกองค์ประกอบนี่ก็หมายความว่าถ้าองค์ประกอบ destructor โยนการทำลายเวกเตอร์ล้มเหลว ... ไม่มีวิธีที่ดีในการป้องกันข้อยกเว้นที่ถูกโยนจาก destructors ดังนั้นไลบรารี ทำให้ไม่มีการค้ำประกันถ้า destructor องค์ประกอบโยน"(จากภาคผนวก E3.2)

บทความนี้ดูเหมือนจะพูดเป็นอย่างอื่น - การขว้างปา destructors นั้นไม่เป็นไร

ดังนั้นคำถามของฉันคือ - หากการขว้างปาจาก destructor ส่งผลให้เกิดพฤติกรรมที่ไม่ได้กำหนดคุณจะจัดการกับข้อผิดพลาดที่เกิดขึ้นระหว่าง destructor ได้อย่างไร

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

เห็นได้ชัดว่าข้อผิดพลาดประเภทนี้หายาก แต่เป็นไปได้


36
"ข้อยกเว้นสองข้อพร้อมกัน" เป็นคำตอบของสต็อก แต่ไม่ใช่เหตุผลที่แท้จริง เหตุผลที่แท้จริงคือข้อยกเว้นควรถูกโยนทิ้งหากไม่สามารถทำตาม postconditions ของฟังก์ชันได้ postcondition ของ destructor คือวัตถุนั้นไม่มีอยู่อีกต่อไป สิ่งนี้ไม่สามารถเกิดขึ้นได้ การดำเนินการสิ้นสุดอายุการใช้งานที่ล้มเหลวของความล้มเหลวจะต้องถูกเรียกว่าเป็นวิธีการแยกต่างหากก่อนที่วัตถุจะออกจากขอบเขต
spraff

29
@spraff: คุณรู้ไหมว่าสิ่งที่คุณพูดมีความหมายว่า "ทิ้ง RAII" หรือไม่?
Kos

16
@ spraff: ต้องเรียกว่า "วิธีการแยกต่างหากก่อนที่วัตถุจะออกนอกขอบเขต" (ตามที่คุณเขียน) จริง ๆ แล้วจะทิ้ง RAII! รหัสที่ใช้วัตถุดังกล่าวจะต้องตรวจสอบให้แน่ใจว่าวิธีการดังกล่าวจะถูกเรียกก่อนที่ตัวทำลายจะถูกเรียก .. ในที่สุดความคิดนี้ก็ไม่ได้ช่วยอะไรเลย
รานซี่

8
@Ferrsi ไม่เพราะปัญหานี้เกิดจากข้อเท็จจริงที่ว่า destructor กำลังพยายามทำบางสิ่งที่นอกเหนือจากการปล่อยทรัพยากรเท่านั้น มันเป็นการล่อลวงที่จะพูดว่า "ฉันต้องการที่จะจบลงด้วยการทำ XYZ" และคิดว่านี่เป็นข้อโต้แย้งในการวางตรรกะเช่นนี้ไว้ในเครื่องทำลายล้าง ไม่ไม่ต้องขี้เกียจเขียนxyz()และรักษาผู้ทำลายล้างของตรรกะที่ไม่ใช่ RAII
spraff

6
@Frunsi ตัวอย่างเช่นการส่งบางสิ่งไปยังไฟล์นั้นไม่จำเป็นว่าจะต้องทำใน destructor ของคลาสที่เป็นตัวแทนของธุรกรรม หากการส่งมอบล้มเหลวสายเกินไปที่จะจัดการได้เมื่อรหัสทั้งหมดที่เกี่ยวข้องในการทำธุรกรรมเกินขอบเขต destructor ควรยกเลิกธุรกรรมยกเว้นว่ามีcommit()การเรียกวิธีการ
นิโคลัสวิลสัน

คำตอบ:


197

การโยนข้อยกเว้นจากผู้ทำลายล้างนั้นเป็นอันตราย
หากข้อยกเว้นอื่นกำลังเผยแพร่แอปพลิเคชันจะยุติลง

#include <iostream>

class Bad
{
    public:
        // Added the noexcept(false) so the code keeps its original meaning.
        // Post C++11 destructors are by default `noexcept(true)` and
        // this will (by default) call terminate if an exception is
        // escapes the destructor.
        //
        // But this example is designed to show that terminate is called
        // if two exceptions are propagating at the same time.
        ~Bad() noexcept(false)
        {
            throw 1;
        }
};
class Bad2
{
    public:
        ~Bad2()
        {
            throw 1;
        }
};


int main(int argc, char* argv[])
{
    try
    {
        Bad   bad;
    }
    catch(...)
    {
        std::cout << "Print This\n";
    }

    try
    {
        if (argc > 3)
        {
            Bad   bad; // This destructor will throw an exception that escapes (see above)
            throw 2;   // But having two exceptions propagating at the
                       // same time causes terminate to be called.
        }
        else
        {
            Bad2  bad; // The exception in this destructor will
                       // cause terminate to be called.
        }
    }
    catch(...)
    {
        std::cout << "Never print this\n";
    }

}

โดยทั่วไปแล้วจะเดือดลงไปที่:

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

destructor จะเสร็จสิ้นการปิดวัตถุโดยการเรียกวิธีการเหล่านี้ (หากผู้ใช้ไม่ได้ทำอย่างชัดเจน) แต่การโยนข้อยกเว้นใด ๆ จะถูกจับและลดลง (หลังจากพยายามที่จะแก้ไขปัญหา)

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

ตัวอย่าง:

มาตรฐาน :: fstream

วิธีการปิด () อาจทำให้เกิดข้อยกเว้น destructor เรียก close () หากไฟล์ถูกเปิด แต่ตรวจสอบให้แน่ใจว่าข้อยกเว้นใด ๆ ไม่ได้เผยแพร่ออกมาจาก destructor

ดังนั้นหากผู้ใช้วัตถุไฟล์ต้องการจัดการเป็นพิเศษสำหรับปัญหาที่เกี่ยวข้องกับการปิดไฟล์พวกเขาจะโทรปิด () ด้วยตนเองและจัดการข้อยกเว้นใด ๆ หากในอีกด้านหนึ่งพวกเขาไม่สนใจผู้ทำลายจะถูกทิ้งไว้ให้จัดการกับสถานการณ์

Scott Myers มีบทความที่ยอดเยี่ยมเกี่ยวกับเรื่องในหนังสือของเขา "Effective C ++"

แก้ไข:

เห็นได้ชัดว่าในรายการ "C ++ ที่มีประสิทธิภาพมากขึ้น"
11: ป้องกันข้อยกเว้นจากการออกจาก destructors


5
"ถ้าคุณไม่รังเกียจที่จะยกเลิกแอปพลิเคชั่นคุณควรจะกลืนข้อผิดพลาด" - นี่น่าจะเป็นข้อยกเว้น (การอภัยโทษ) มากกว่ากฎ - นั่นคือการล้มเหลวอย่างรวดเร็ว
Erik Forbes

15
ฉันไม่เห็นด้วย. การสิ้นสุดโปรแกรมจะหยุดการคลายสแต็ก จะไม่มีการเรียกตัวทำลายอีกต่อไป ทรัพยากรใด ๆ ที่เปิดจะถูกเปิดทิ้งไว้ ฉันคิดว่าการกลืนข้อยกเว้นจะเป็นตัวเลือกที่ต้องการ
Martin York

20
ระบบปฏิบัติการทำความสะอาดทรัพยากรที่เป็นเจ้าของ หน่วยความจำ FileHandles ฯลฯ สิ่งที่เกี่ยวกับทรัพยากรที่ซับซ้อน: การเชื่อมต่อฐานข้อมูล อัปลิงค์นั้นไปยังสถานีอวกาศนานาชาติที่คุณเปิด (มันจะส่งการเชื่อมต่ออย่างใกล้ชิดโดยอัตโนมัติ) หรือไม่? ฉันแน่ใจว่านาซ่าต้องการให้คุณปิดการเชื่อมต่ออย่างหมดจด!
Martin York

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

2
@LokiAstari โปรโตคอลการขนส่งที่คุณใช้สื่อสารกับยานอวกาศไม่สามารถจัดการกับการเชื่อมต่อที่ถูกทิ้งได้หรือไม่? ตกลง ...
doug65536

54

การส่งออกจาก destructor อาจส่งผลให้เกิดความผิดพลาดได้เนื่องจาก destructor นี้อาจถูกเรียกว่าเป็นส่วนหนึ่งของ "Stack unwinding" สแต็คคลี่คลายเป็นกระบวนการที่เกิดขึ้นเมื่อมีข้อยกเว้นจะถูกโยน ในขั้นตอนนี้วัตถุทั้งหมดที่ถูกผลักเข้าไปในกองซ้อนตั้งแต่ "ลอง" และจนกว่าข้อยกเว้นจะถูกโยนจะถูกยกเลิก -> destructors ของพวกเขาจะถูกเรียก และในระหว่างขั้นตอนนี้ไม่อนุญาตให้มีการโยนข้อยกเว้นอีกครั้งเนื่องจากไม่สามารถจัดการกับข้อยกเว้นได้สองข้อในแต่ละครั้งดังนั้นสิ่งนี้จะกระตุ้นการเรียกร้องให้ยกเลิก () โปรแกรมจะหยุดทำงานและการควบคุมจะกลับไปที่ระบบปฏิบัติการ


1
คุณช่วยอธิบายเพิ่มเติมเกี่ยวกับการยกเลิก () ได้อย่างไรในสถานการณ์ข้างต้น หมายถึงการควบคุมการดำเนินการยังคงอยู่กับคอมไพเลอร์ C ++
กฤษณะ Oza

1
@Krishna_Oza: ค่อนข้างง่าย: เมื่อใดก็ตามที่เกิดข้อผิดพลาดรหัสที่ยกข้อผิดพลาดตรวจสอบบิตที่ระบุว่าระบบรันไทม์อยู่ในกระบวนการของการคลี่คลายสแตก (เช่นการจัดการอื่น ๆthrowแต่ยังไม่พบcatchบล็อกเลย) ในกรณีที่std::terminate(ไม่abort) ถูกเรียกแทนการเพิ่มข้อยกเว้น (ใหม่) (หรือดำเนินการต่อการคลายสแต็ก)
Marc van Leeuwen

53

เราต้องแยกความแตกต่างที่นี่แทนการทำตามคำแนะนำทั่วไปสำหรับกรณีเฉพาะ

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

ปัญหาทั้งหมดจะง่ายขึ้นเมื่อเราแบ่งชั้นเรียนออกเป็นสองประเภท คลาส dtor สามารถมีความรับผิดชอบที่แตกต่างกันสองแบบ:

  • (R) ปลดปล่อยซีแมนทิกส์ (ฟรีหน่วยความจำนั้น)
  • (C) กระทำความหมาย (aka ล้างไฟล์ไปยังดิสก์)

ถ้าเราดูคำถามด้วยวิธีนี้ฉันคิดว่ามันอาจเป็นที่ถกเถียงกันอยู่ว่าความหมาย (R) ไม่ควรทำให้เกิดข้อยกเว้นจาก dtor เนื่องจากมี) เราไม่สามารถทำอะไรกับมันได้และ b) การดำเนินการทรัพยากรฟรีจำนวนมากไม่ แม้จะจัดให้มีการตรวจสอบข้อผิดพลาดเช่นvoid free(void* p);

วัตถุที่มี (C) ซีแมนทิกส์เหมือนวัตถุไฟล์ที่ต้องล้างข้อมูลให้สำเร็จหรือเชื่อมต่อฐานข้อมูล ("ขอบเขตการป้องกัน") ที่กระทำการใน dtor นั้นแตกต่างกัน: เราสามารถทำบางอย่างเกี่ยวกับข้อผิดพลาด (บน ระดับแอปพลิเคชัน) และเราไม่ควรดำเนินการต่อไปเหมือนไม่มีอะไรเกิดขึ้น

หากเราไปตามเส้นทาง RAII และอนุญาตให้วัตถุที่มีความหมาย (C) ใน d'tors ของพวกเขาฉันคิดว่าเราก็ต้องอนุญาตให้มีกรณีแปลก ๆ ที่ d'tors ดังกล่าวสามารถโยนได้ เป็นไปตามที่คุณไม่ควรใส่วัตถุดังกล่าวลงในภาชนะบรรจุและยังเป็นไปตามที่โปรแกรมสามารถยังคงได้terminate()หากการกระทำที่กระทำผิดเกิดขึ้นในขณะที่มีข้อยกเว้นอื่นทำงานอยู่


เกี่ยวกับการจัดการข้อผิดพลาด (ความหมาย Commit / Rollback) และข้อยกเว้นมีการพูดคุยที่ดีโดยAndrei Alexandrescuหนึ่งคน: การจัดการข้อผิดพลาดใน C ++ / Declarative Control Flow (จัดขึ้นที่NDC 2014 )

ในรายละเอียดของเขาอธิบายถึงวิธีการดำเนินการห้องสมุดเขลาUncaughtExceptionCounterสำหรับพวกเขาScopeGuardขับรถ

(ฉันควรทราบว่าคนอื่น ๆก็มีความคิดคล้าย ๆ กัน)

ในขณะที่การพูดคุยไม่ได้มุ่งเน้นไปที่การขว้างปาจาก d'tor มันแสดงให้เห็นถึงเครื่องมือที่สามารถนำมาใช้ในวันนี้เพื่อกำจัดปัญหาด้วยเมื่อจะโยนจาก d'tor

ในอนาคตมีอาจจะเป็นคุณลักษณะมาตรฐานนี้ดูN3614 ,และการอภิปรายเกี่ยวกับเรื่องนี้

อัปเดต '17: คุณลักษณะ C ++ 17 std สำหรับสิ่งนี้คือstd::uncaught_exceptionsafaikt ฉันจะอ้างบทความ cppref อย่างรวดเร็ว:

หมายเหตุ

ตัวอย่างที่int-returning uncaught_exceptionsถูกนำมาใช้เป็นครั้งแรก ... ... สร้างวัตถุยามและบันทึกจำนวนข้อยกเว้น uncaught ในคอนสตรัคของมัน เอาต์พุตดำเนินการโดย destructor ของวัตถุยามยกเว้น foo () โยน ( ในกรณีนี้จำนวนข้อยกเว้นที่ไม่ได้ตรวจสอบใน destructor นั้นมากกว่าสิ่งที่ผู้สังเกตเห็น )


6
เห็นด้วยอย่างยิ่ง และเพิ่มซีแมนทิกส์การย้อนกลับความหมาย (Ro) อีกหนึ่งความหมาย ใช้กันทั่วไปในขอบเขตยาม เช่นกรณีในโครงการของฉันที่ฉันกำหนดแมโคร ON_SCOPE_EXIT กรณีเกี่ยวกับความหมายย้อนกลับคือสิ่งที่มีความหมายอาจเกิดขึ้นที่นี่ ดังนั้นเราไม่ควรมองข้ามความล้มเหลว
Weipeng L

ฉันรู้สึกเหมือนเป็นเหตุผลเดียวที่เราได้กระทำความหมายใน destructors คือ C ++ finallyไม่สนับสนุน
user541686

@ Mehrdad: finally เป็น dtor มันถูกเรียกเสมอว่าไม่ว่าอะไรจะเกิดขึ้น สำหรับการประมาณวากยสัมพันธ์เกี่ยวกับวากยสัมพันธ์ดูที่การประยุกต์ใช้ scope_guard ต่างๆ ทุกวันนี้ด้วยเครื่องจักรในสถานที่ (แม้จะอยู่ในมาตรฐานมันคือ C ++ 14?) เพื่อตรวจสอบว่า dtor ได้รับอนุญาตให้โยนหรือไม่มันสามารถทำให้ปลอดภัยโดยสิ้นเชิง
Martin Ba

1
@MartinBa: ฉันคิดว่าคุณพลาดจุดแสดงความคิดเห็นของฉันซึ่งน่าประหลาดใจเนื่องจากฉันเห็นด้วยกับความคิดของคุณว่า (R) และ (C) แตกต่างกัน ฉันพยายามที่จะบอกว่า dtor เป็นเครื่องมือสำหรับ (R) และfinallyเป็นเครื่องมือสำหรับ (C) หากคุณไม่เห็นเหตุผล: พิจารณาว่าทำไมมันถูกต้องตามกฎหมายที่จะโยนข้อยกเว้นด้านบนของแต่ละอื่น ๆ ในfinallyบล็อกและเหตุผลเดียวคือไม่ได้สำหรับ destructors (ในบางแง่มันเป็นข้อมูลเทียบกับสิ่งควบคุม ) Destructors สำหรับการปล่อยข้อมูลfinallyเป็นการปล่อยการควบคุมพวกมันต่างกันโชคร้ายที่ C ++ เชื่อมโยงพวกเขาเข้าด้วยกัน)
user541686

1
@Mehrdad: นี่นานเกินไป ถ้าคุณต้องการคุณสามารถสร้างข้อโต้แย้งของคุณที่นี่: programmers.stackexchange.com/questions/304067/... ขอบคุณ
Martin Ba

21

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

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


11
เพิ่งสังเกตุ ... การขว้างจาก dtor นั้นไม่เคยมีพฤติกรรมที่ไม่ได้กำหนด แน่นอนว่ามันอาจจะเรียกว่า terminate () แต่นั่นเป็นพฤติกรรมที่ระบุไว้เป็นอย่างดี
Martin Ba

4
std::ofstreamdestructor ของ flushes แล้วปิดแฟ้ม ข้อผิดพลาดของดิสก์เต็มอาจเกิดขึ้นในขณะที่ฟลัชชิงซึ่งคุณสามารถทำสิ่งที่มีประโยชน์กับ: แสดงกล่องโต้ตอบข้อผิดพลาดให้ผู้ใช้ทราบว่าดิสก์มีเนื้อที่ว่างไม่เพียงพอ
แอนดี้

13

มันอันตราย แต่ก็ไม่ได้ทำให้เข้าใจได้จากมุมมองความสามารถในการอ่าน / รหัส

สิ่งที่คุณต้องถามอยู่ในสถานการณ์นี้

int foo()
{
   Object o;
   // As foo exits, o's destructor is called
}

สิ่งที่ควรจับยกเว้น? ผู้โทรของ foo ควรหรือไม่ หรือ foo ควรจัดการกับมัน? ทำไมผู้โทรของ foo จึงสนใจเกี่ยวกับวัตถุบางอย่างภายในเพื่อ foo อาจมีวิธีที่ภาษากำหนดสิ่งนี้เพื่อให้เข้าใจได้ แต่มันจะอ่านไม่ออกและเข้าใจยาก

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

พิจารณากรณีนี้

class Object
{ 
   Object2 obj2;
   Object3* obj3;
   virtual ~Object()
   {
       // What should happen when this fails? How would I actually destroy this?
       delete obj3;

       // obj 2 fails to destruct when it goes out of scope, now what!?!?
       // should the exception propogate? 
   } 
};

เมื่อการลบ obj3 ล้มเหลวฉันจะลบอย่างไรในแบบที่รับประกันว่าจะไม่ล้มเหลว? มันเป็นความทรงจำของฉันเลย!

ในตอนนี้ให้พิจารณาในตัวอย่างโค้ดแรกซึ่ง Object จะหายไปโดยอัตโนมัติ เนื่องจากตัวชี้ไปยัง Object3 หายไปคุณเป็นคนประเภท SOL คุณมีหน่วยความจำรั่ว

ตอนนี้วิธีที่ปลอดภัยวิธีหนึ่งในการทำสิ่งต่าง ๆ ต่อไปนี้คือ

class Socket
{
    virtual ~Socket()
    {
      try 
      {
           Close();
      }
      catch (...) 
      {
          // Why did close fail? make sure it *really* does close here
      }
    } 

};

ดูคำถามที่พบบ่อยนี้ด้วย


รื้อฟื้นคำตอบนี้ใหม่: ตัวอย่างแรกเกี่ยวกับint foo()คุณสามารถใช้ฟังก์ชั่นลองบล็อกเพื่อห่อฟังก์ชั่นทั้งหมดในบล็อกลองจับรวมถึงการจับ destructors ถ้าคุณสนใจที่จะทำเช่นนั้น ยังไม่ได้เป็นวิธีที่ต้องการ แต่มันเป็นสิ่งที่
tyree731

13

จากแบบร่าง ISO สำหรับ C ++ (ISO / IEC JTC 1 / SC 22 N 4411)

ดังนั้นโดยทั่วไปแล้ว destructors ควรตรวจจับข้อยกเว้นและไม่ให้พวกมันแพร่กระจายออกจากตัวทำลาย

3 กระบวนการของการเรียก destructors สำหรับวัตถุอัตโนมัติที่สร้างขึ้นบนเส้นทางจากบล็อกลองไปจนถึงการโยนเรียกว่า“ กองคลี่คลาย” [หมายเหตุ: หากตัวทำลายถูกเรียกในระหว่างการออกจากสแต็กโดยมีข้อยกเว้น std :: ยุติจะถูกเรียก (15.5.1) ดังนั้นโดยทั่วไปแล้ว destructors ควรตรวจจับข้อยกเว้นและไม่ให้พวกมันแพร่กระจายออกจากตัวทำลาย - บันทึกท้าย]


1
ไม่ได้ตอบคำถาม - OP ทราบแล้วในเรื่องนี้
Arafangion

2
@Arafangion ฉันสงสัยว่าเขารู้เรื่องนี้ (std :: ยุติการถูกเรียก) เป็นคำตอบที่ยอมรับทำให้ตรงประเด็นเดียวกัน
lothar

@Arafangion ในบางคำตอบที่นี่บางคนพูดถึงว่ายกเลิก () ถูกเรียก; หรือมันเป็นที่ std :: ยุติในการเรียกสายยกเลิก () ฟังก์ชั่น
กฤษณะ Oza

7

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


7

ฉันอยู่ในกลุ่มที่คิดว่ารูปแบบ "scoped guard" การขว้างปาใน destructor นั้นมีประโยชน์ในหลาย ๆ สถานการณ์ - โดยเฉพาะอย่างยิ่งสำหรับการทดสอบหน่วย อย่างไรก็ตามทราบว่าใน C ++ 11 ขว้างปาในผล destructor ในการเรียกร้องให้std::terminateตั้งแต่ destructors noexceptมีบันทึกย่อโดยปริยายด้วย

Andrzej Krzemieńskiมีโพสต์ที่ยอดเยี่ยมในหัวข้อของ destructors ที่โยน:

เขาชี้ให้เห็นว่า C ++ 11 มีกลไกในการแทนที่ค่าเริ่มต้นnoexceptสำหรับ destructors:

ใน C ++ 11 destructor noexceptระบุไว้โดยปริยายเป็น แม้ว่าคุณจะไม่เพิ่มข้อมูลจำเพาะและกำหนด destructor ของคุณเช่นนี้:

  class MyType {
        public: ~MyType() { throw Exception(); }            // ...
  };

คอมไพเลอร์จะยังคงเพิ่มสเปคnoexceptให้กับ destructor ของคุณอย่างล่องหน และนี่หมายความว่าช่วงเวลาที่ผู้ทำลายล้างของคุณขว้างข้อยกเว้นstd::terminateจะถูกเรียกแม้ว่าจะไม่มีสถานการณ์ยกเว้นสองครั้งก็ตาม หากคุณตั้งใจแน่วแน่ที่จะอนุญาตให้ผู้ทำลายล้างของคุณโยนคุณจะต้องระบุสิ่งนี้อย่างชัดเจน คุณมีสามตัวเลือก:

  • ระบุอย่างชัดเจน destructor ของคุณเป็นnoexcept(false),
  • สืบทอดคลาสของคุณจากคลาสอื่นที่ระบุ destructor เป็นnoexcept(false)แล้ว
  • ใส่สมาชิกข้อมูลที่ไม่คงที่ในชั้นเรียนของคุณที่ระบุ destructor ของมันnoexcept(false)แล้ว

ในที่สุดหากคุณตัดสินใจที่จะโยนใน destructor คุณควรตระหนักถึงความเสี่ยงของการยกเว้นสองครั้ง (การขว้างในขณะที่สแต็กกำลังผ่อนคลายเนื่องจากข้อยกเว้น) นี่จะทำให้มีการโทรออกstd::terminateและไม่ค่อยเป็นสิ่งที่คุณต้องการ std::uncaught_exception()เพื่อหลีกเลี่ยงปัญหานี้คุณก็สามารถตรวจสอบได้ว่ามีอยู่แล้วให้มีการยกเว้นก่อนที่จะโยนขึ้นมาใหม่โดยใช้


6

ทุกคนอธิบายว่าทำไมการขว้างปา destructors นั้นแย่มาก ... คุณทำอะไรได้บ้าง? หากคุณกำลังทำการดำเนินการที่อาจล้มเหลวให้สร้างวิธีสาธารณะแยกต่างหากที่ดำเนินการล้างข้อมูลและสามารถส่งข้อยกเว้นตามอำเภอใจได้ ในกรณีส่วนใหญ่ผู้ใช้จะไม่สนใจสิ่งนั้น หากผู้ใช้ต้องการตรวจสอบความสำเร็จ / ความล้มเหลวของการล้างข้อมูลพวกเขาสามารถเรียกรูทีนการล้างอย่างชัดเจน

ตัวอย่างเช่น:

class TempFile {
public:
    TempFile(); // throws if the file couldn't be created
    ~TempFile() throw(); // does nothing if close() was already called; never throws
    void close(); // throws if the file couldn't be deleted (e.g. file is open by another process)
    // the rest of the class omitted...
};

ฉันกำลังมองหาวิธีแก้ปัญหา แต่พวกเขาพยายามอธิบายว่าเกิดอะไรขึ้นและทำไม แค่อยากให้ชัดเจนว่าฟังก์ชั่นการปิดถูกเรียกภายในตัวทำลายหรือไม่?
Jason Liu

5

นอกเหนือจากคำตอบหลักซึ่งดีครอบคลุมและถูกต้องแล้วฉันต้องการแสดงความคิดเห็นเกี่ยวกับบทความที่คุณอ้างอิง - บทความที่กล่าวว่า "การโยนข้อยกเว้นใน destructors นั้นไม่เลว"

บทความใช้บรรทัด "สิ่งที่เป็นทางเลือกในการโยนข้อยกเว้น" และรายการปัญหาบางอย่างกับแต่ละทางเลือก หลังจากทำเสร็จแล้วจึงสรุปได้ว่าเนื่องจากเราไม่สามารถหาทางเลือกที่ปราศจากปัญหาได้เราจึงควรทำการยกเว้นต่อไป

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


1
ไม่ใช่พฤติกรรมที่ไม่ได้กำหนด แต่เป็นการเลิกทันที
Marc van Leeuwen

มาตรฐานระบุว่า 'พฤติกรรมที่ไม่ได้กำหนด' พฤติกรรมนั้นมักถูกยกเลิก แต่ก็ไม่เสมอไป
DJClayworth

ไม่อ่าน [exception.terminate] ในการจัดการข้อยกเว้น -> ฟังก์ชั่นพิเศษ (ซึ่งคือ 15.5.1 ในสำเนามาตรฐานของฉัน แต่การกำหนดหมายเลขอาจล้าสมัย)
Marc van Leeuwen

2

คำถาม: ดังนั้นคำถามของฉันคือ - ถ้าการขว้างปาจาก destructor ส่งผลให้เกิดพฤติกรรมที่ไม่ได้กำหนดคุณจะจัดการกับข้อผิดพลาดที่เกิดขึ้นระหว่าง destructor ได้อย่างไร

ตอบ: มีหลายตัวเลือก:

  1. ปล่อยให้ข้อยกเว้นไหลออกจากตัวทำลายของคุณโดยไม่คำนึงถึงสิ่งที่เกิดขึ้นที่อื่น และในการทำเช่นนั้นให้ระวัง (หรือแม้แต่หวาดกลัว) ที่ std :: ยุติอาจตามมา

  2. อย่าปล่อยให้ข้อยกเว้นไหลออกจากตัวทำลายของคุณ อาจเขียนลงในบันทึกข้อความที่ไม่ดีสีแดงขนาดใหญ่ถ้าคุณทำได้

  3. fave ของฉัน : ถ้าstd::uncaught_exceptionส่งคืนเท็จให้ข้อยกเว้นไหลออกมา ถ้ามันกลับมาจริงแล้วถอยกลับไปที่วิธีการเข้าสู่ระบบ

แต่มันเป็นการดีไหมที่จะโยนใน d'tors?

ฉันเห็นด้วยกับสิ่งที่กล่าวมาแล้วข้างต้นว่าการขว้างปาเป็นสิ่งที่หลีกเลี่ยงได้ดีที่สุดในผู้ทำลายล้าง แต่บางครั้งคุณก็ยอมรับได้ดีที่สุดว่าสามารถเกิดขึ้นได้และจัดการได้ดี ฉันเลือก 3 ข้อ

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


4
สิ่งที่คุณชอบคือสิ่งที่ฉันได้ลองเมื่อไม่นานมานี้และปรากฎว่าคุณไม่ควรทำ gotw.ca/gotw/047.htm
GManNickG

1

ขณะนี้ฉันปฏิบัติตามนโยบาย (ที่มีคนพูดกันมาก) ว่าชั้นเรียนไม่ควรโยนข้อยกเว้นจากผู้ทำลายล้างของพวกเขา แต่ควรจัดให้มีวิธี "ปิด" สาธารณะเพื่อทำการดำเนินการที่อาจล้มเหลว ...

... แต่ฉันเชื่อว่า destructors สำหรับคลาส container-type เช่น vector ไม่ควรปิดบังข้อยกเว้นที่เกิดจากคลาสที่มีอยู่ ในกรณีนี้ฉันใช้วิธี "ฟรี / ปิด" ที่เรียกตัวเองซ้ำ ๆ ใช่ฉันพูดซ้ำแล้วซ้ำอีก มีวิธีการบ้านี้ การแพร่กระจายข้อยกเว้นขึ้นอยู่กับว่ามีสแต็ก: หากมีข้อยกเว้นเดียวเกิดขึ้นทั้ง destructors ที่เหลือจะยังคงทำงานและข้อยกเว้นที่รอดำเนินการจะเผยแพร่เมื่อรูทีนกลับมาซึ่งยอดเยี่ยม หากมีข้อยกเว้นหลายอย่างเกิดขึ้นแล้ว (ขึ้นอยู่กับคอมไพเลอร์) อาจเป็นข้อยกเว้นแรกที่จะเผยแพร่หรือโปรแกรมจะยุติซึ่งก็โอเค หากมีข้อยกเว้นมากมายเกิดขึ้นที่การเรียกซ้ำเกินกว่าสแต็กนั้นมีบางอย่างผิดปกติอย่างยิ่งและบางคนกำลังหาข้อมูลเกี่ยวกับมันซึ่งก็โอเคเช่นกัน ส่วนตัว,

ประเด็นก็คือภาชนะบรรจุยังคงเป็นกลางและขึ้นอยู่กับชั้นเรียนที่มีอยู่เพื่อตัดสินว่าพวกเขาประพฤติตนหรือประพฤติมิชอบในเรื่องการขว้างข้อยกเว้นจากผู้ทำลายล้าง


1

ต่างจากคอนสตรัคเตอร์ซึ่งการขว้างข้อยกเว้นอาจเป็นวิธีที่มีประโยชน์ในการบ่งชี้ว่าการสร้างวัตถุสำเร็จแล้วข้อยกเว้นไม่ควรถูกโยนลงใน destructors

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

ดังนั้นแนวทางการปฏิบัติที่ดีที่สุดคืองดเว้นจากการใช้ข้อยกเว้นใน destructors ทั้งหมด เขียนข้อความไปยังไฟล์บันทึกแทน


1
การเขียนข้อความไปยังล็อกไฟล์อาจทำให้เกิดข้อยกเว้น
Konard

1

Martin Ba (ด้านบน) อยู่ในแนวทางที่ถูกต้อง - สถาปนิกของคุณแตกต่างกันสำหรับ RELEASE และตรรกะ COMMIT

สำหรับการเปิดตัว:

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

สำหรับความมุ่งมั่น:

นี่คือที่ที่คุณต้องการวัตถุห่อหุ้ม RAII ชนิดเดียวกับที่ std :: lock_guard เตรียมไว้สำหรับ mutexes กับสิ่งที่คุณไม่ได้ใส่ความมุ่งมั่นใน dtor เลย คุณมี API เฉพาะสำหรับมันแล้วห่อหุ้มวัตถุที่ RAII จะยอมรับมันใน dtors ของพวกเขาและจัดการข้อผิดพลาดที่นั่น จำไว้ว่าคุณสามารถจับข้อยกเว้นในตัวทำลายได้ดี มันออกพวกเขาที่ร้ายแรง นอกจากนี้ยังช่วยให้คุณสามารถใช้นโยบายและการจัดการข้อผิดพลาดที่แตกต่างกันเพียงแค่สร้าง wrapper ที่แตกต่างกัน (เช่น std :: unique_lock vs. std :: lock_guard) และให้แน่ใจว่าคุณจะไม่ลืมที่จะเรียกตรรกะการกระทำ - ซึ่งเป็นเพียงครึ่งทาง เหตุผลที่ดีสำหรับการวางไว้ใน dtor ในสถานที่ที่ 1


1

ดังนั้นคำถามของฉันคือ - หากการขว้างปาจาก destructor ส่งผลให้เกิดพฤติกรรมที่ไม่ได้กำหนดคุณจะจัดการกับข้อผิดพลาดที่เกิดขึ้นระหว่าง destructor ได้อย่างไร

ปัญหาหลักคือ: คุณไม่สามารถล้มเหลวที่จะล้มเหลว หลังจากล้มเหลวหมายความว่าอย่างไร หากการทำธุรกรรมกับฐานข้อมูลล้มเหลวและล้มเหลวในการล้มเหลว (ล้มเหลวในการย้อนกลับ) จะเกิดอะไรขึ้นกับความสมบูรณ์ของข้อมูลของเรา

เนื่องจาก destructors ถูกเรียกใช้สำหรับพา ธ ทั้งปกติและพิเศษ (ล้มเหลว) พวกเขาจึงไม่สามารถล้มเหลวหรืออย่างอื่นที่เรา "ล้มเหลวในการล้มเหลว"

นี่เป็นปัญหาที่เกิดจากแนวคิดรวบยอด แต่บ่อยครั้งวิธีแก้ไขก็คือการหาวิธีที่จะทำให้แน่ใจว่าการล้มเหลวนั้นไม่สามารถล้มเหลวได้ ตัวอย่างเช่นฐานข้อมูลอาจเขียนการเปลี่ยนแปลงก่อนที่จะกระทำกับโครงสร้างข้อมูลภายนอกหรือไฟล์ หากการทำธุรกรรมล้มเหลวโครงสร้างไฟล์ / ข้อมูลสามารถถูกโยนทิ้งได้ สิ่งที่ต้องทำคือให้แน่ใจว่าการยอมรับการเปลี่ยนแปลงจากโครงสร้าง / ไฟล์ภายนอกนั้นเป็นธุรกรรมอะตอมมิกที่ไม่สามารถล้มเหลวได้

วิธีแก้ปัญหาในทางปฏิบัติอาจเป็นเพียงการทำให้แน่ใจว่าโอกาสในการล้มเหลวจากความล้มเหลวนั้นไม่น่าเป็นไปได้ทางดาราศาสตร์เนื่องจากการทำสิ่งที่เป็นไปไม่ได้ให้ล้มเหลวนั้นเป็นไปไม่ได้ในบางกรณี

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

ทั้งหมดนี้พูดได้ง่ายกว่าทำเสร็จเป็นที่ยอมรับ แต่เป็นวิธีเดียวที่ฉันเห็นว่าเหมาะสม บางครั้งฉันคิดว่าควรมีความสามารถในการเขียนตรรกะ destructor ที่แยกต่างหากสำหรับเส้นทางการดำเนินการปกติออกไปจากคนพิเศษเนื่องจากบางครั้ง destructors รู้สึกเล็กน้อยเหมือนพวกเขามีความรับผิดชอบเป็นสองเท่าโดยพยายามที่จะจัดการทั้งสอง (ตัวอย่างคือ พวกเขาไม่ต้องการสิ่งนี้หากพวกเขาสามารถแยกแยะเส้นทางการทำลายที่ไม่ธรรมดาจากที่ไม่ได้รับการยกเว้น)

ปัญหาที่สำคัญที่สุดคือเราไม่สามารถล้มเหลวได้และเป็นปัญหาการออกแบบแนวคิดที่ยากที่จะแก้ไขอย่างสมบูรณ์ในทุกกรณี มันจะง่ายขึ้นถ้าคุณไม่ห่อหุ้มโครงสร้างการควบคุมที่ซับซ้อนด้วยวัตถุเล็ก ๆ จำนวนมากที่มีปฏิสัมพันธ์ซึ่งกันและกันและแทนที่จะสร้างแบบจำลองการออกแบบของคุณในแบบที่มีลักษณะเป็นกลุ่มเล็กน้อย (ตัวอย่าง: ระบบอนุภาคที่มีตัวทำลายเพื่อทำลายอนุภาคทั้งหมด ระบบไม่ใช่ destructor แบบไม่แยกย่อยต่ออนุภาค) เมื่อคุณสร้างแบบจำลองการออกแบบของคุณในระดับ coarser เช่นนี้คุณมี destructors ที่ไม่น่ารำคาญน้อยกว่าที่จะจัดการกับและยังสามารถจ่ายสิ่งที่หน่วยความจำ / การประมวลผลมักจะต้องใช้เพื่อให้แน่ใจว่า destructors ของคุณไม่สามารถล้มเหลว

และนั่นเป็นหนึ่งในวิธีแก้ปัญหาที่ง่ายที่สุดตามธรรมชาติคือการใช้ destructors น้อยกว่าบ่อยครั้ง ในตัวอย่างของอนุภาคด้านบนอาจเป็นการทำลาย / กำจัดอนุภาคบางสิ่งที่ควรทำซึ่งอาจล้มเหลวได้ไม่ว่าด้วยเหตุผลใดก็ตาม ในกรณีดังกล่าวแทนที่จะเรียกใช้ตรรกะดังกล่าวผ่าน dtor ของอนุภาคซึ่งสามารถดำเนินการในเส้นทางที่พิเศษคุณสามารถทำได้โดยระบบอนุภาคทั้งหมดเมื่อมันกำจัดอนุภาค การกำจัดอนุภาคอาจทำได้ตลอดเวลาในเส้นทางที่ไม่ธรรมดา หากระบบถูกทำลายอาจเป็นเพียงการล้างอนุภาคทั้งหมดและไม่ต้องกังวลกับตรรกะการกำจัดอนุภาคที่สามารถล้มเหลวในขณะที่ตรรกะที่สามารถล้มเหลวจะดำเนินการเฉพาะในระหว่างการดำเนินการตามปกติของระบบอนุภาคเมื่อเอาอนุภาคหนึ่งหรือมากกว่า

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

มันจะช่วยได้มากถ้า nothrow / noexcept แปลจริงเป็นข้อผิดพลาดของคอมไพเลอร์หากมีสิ่งใดที่ระบุไว้ (รวมถึงฟังก์ชั่นเสมือนจริงซึ่งควรสืบทอดข้อกำหนด noexcept ของคลาสพื้นฐาน) พยายามเรียกใช้สิ่งใดก็ตามที่สามารถโยนได้ วิธีนี้เราจะสามารถรวบรวมข้อมูลทั้งหมดนี้ได้ในเวลารวบรวมถ้าเราเขียน destructor โดยไม่ตั้งใจซึ่งสามารถโยนได้


1
การทำลายล้มเหลวทันทีหรือไม่
curiousguy

ฉันคิดว่าเขาหมายถึงว่า destructors ถูกเรียกระหว่างความล้มเหลวเพื่อล้างความล้มเหลวนั้น ดังนั้นหากมีการเรียก destructor ระหว่างข้อยกเว้นที่ใช้งานอยู่มันจะล้มเหลวในการล้างข้อมูลจากความล้มเหลวก่อนหน้านี้
user2445507

0

ตั้งค่าเหตุการณ์การเตือน โดยทั่วไปแล้วเหตุการณ์การเตือนภัยจะเป็นรูปแบบที่ดีกว่าในการแจ้งเตือนความล้มเหลวในขณะที่ล้างวัตถุ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.