ném ngoại lệ ra khỏi một kẻ hủy diệt


257

Hầu hết mọi người nói không bao giờ ném ngoại lệ ra khỏi kẻ hủy diệt - làm như vậy dẫn đến hành vi không xác định. Stroustrup đưa ra quan điểm rằng "công cụ phá hủy vectơ gọi rõ ràng công cụ hủy diệt cho mọi phần tử. Điều này ngụ ý rằng nếu một phần tử hủy bỏ ném, việc phá hủy vectơ thất bại ... Thực sự không có cách nào tốt để bảo vệ chống lại các ngoại lệ được ném ra từ các phần tử hủy diệt, vì vậy thư viện không đảm bảo nếu bộ hủy phần tử ném "(từ Phụ lục E3.2) .

Bài viết này dường như nói khác - rằng ném phá hủy ít nhiều không sao.

Vì vậy, câu hỏi của tôi là thế này - nếu ném từ một hàm hủy dẫn đến hành vi không xác định, làm thế nào để bạn xử lý các lỗi xảy ra trong một hàm hủy?

Nếu xảy ra lỗi trong quá trình dọn dẹp, bạn có bỏ qua không? Nếu đó là một lỗi có khả năng có thể được xử lý ngăn xếp nhưng không đúng trong hàm hủy, thì có nghĩa là không ném ngoại lệ ra khỏi hàm hủy không?

Rõ ràng những loại lỗi này là rất hiếm, nhưng có thể.


36
"Hai ngoại lệ cùng một lúc" là một câu trả lời chứng khoán nhưng đó không phải là lý do THỰC SỰ. Lý do thực sự là một ngoại lệ nên được ném nếu và chỉ khi các điều kiện hậu của hàm không thể được đáp ứng. Điều kiện hậu của một hàm hủy là đối tượng không còn tồn tại. Điều này không thể xảy ra. Do đó, bất kỳ hoạt động cuối đời dễ bị lỗi nào cũng phải được gọi là một phương thức riêng biệt trước khi đối tượng đi ra khỏi phạm vi (các hàm hợp lý thường chỉ có một đường dẫn thành công).
spraff

29
@spraff: Bạn có biết rằng những gì bạn nói ngụ ý "vứt bỏ RAII" không?
Kos

16
@spraff: phải gọi "một phương thức riêng biệt trước khi đối tượng đi ra khỏi phạm vi" (như bạn đã viết) thực sự ném đi RAII! Mã sử ​​dụng các đối tượng như vậy sẽ phải đảm bảo rằng một phương thức như vậy sẽ được gọi trước khi hàm hủy được gọi .. Cuối cùng, ý tưởng này không giúp ích gì cả.
Frunsi

8
@Frunsi không, bởi vì vấn đề này xuất phát từ thực tế là kẻ hủy diệt đang cố gắng làm một cái gì đó vượt ra ngoài việc giải phóng tài nguyên. Thật hấp dẫn khi nói "tôi luôn muốn kết thúc việc làm XYZ" và nghĩ rằng đây là một lập luận cho việc đưa logic như vậy vào hàm hủy. Không, đừng lười biếng, hãy viết xyz()và giữ cho kẻ hủy diệt sạch logic phi RAII.
spraff

6
@Frunsi Ví dụ, cam kết một cái gì đó để nộp không nhất thiết phải làm trong hàm hủy của một lớp đại diện cho một giao dịch. Nếu cam kết thất bại, đã quá muộn để xử lý nó khi tất cả các mã liên quan đến giao dịch đã vượt quá phạm vi. Hàm hủy sẽ hủy giao dịch trừ khi commit()phương thức được gọi.
Nicholas Wilson

Câu trả lời:


198

Ném một ngoại lệ ra khỏi một kẻ hủy diệt là nguy hiểm.
Nếu một ngoại lệ khác đã được tuyên truyền, ứng dụng sẽ chấm dứt.

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

}

Điều này về cơ bản sôi xuống:

Bất cứ điều gì nguy hiểm (nghĩa là có thể ném ngoại lệ) nên được thực hiện thông qua các phương thức công khai (không nhất thiết là trực tiếp). Người dùng của lớp của bạn sau đó có thể có khả năng xử lý các tình huống này bằng cách sử dụng các phương thức công khai và nắm bắt bất kỳ trường hợp ngoại lệ tiềm năng nào.

Sau đó, hàm hủy sẽ kết thúc đối tượng bằng cách gọi các phương thức này (nếu người dùng không làm như vậy một cách rõ ràng), nhưng bất kỳ trường hợp ngoại lệ nào cũng bị bắt và bỏ (sau khi cố gắng khắc phục sự cố).

Vì vậy, trong thực tế, bạn chuyển trách nhiệm cho người dùng. Nếu người dùng ở vị trí để sửa các ngoại lệ, họ sẽ gọi thủ công các chức năng phù hợp và xử lý bất kỳ lỗi nào. Nếu người dùng của đối tượng không lo lắng (vì đối tượng sẽ bị phá hủy) thì kẻ hủy diệt sẽ được giao nhiệm vụ kinh doanh.

Một ví dụ:

std :: dòng

Phương thức close () có khả năng đưa ra một ngoại lệ. Hàm hủy gọi close () nếu tệp đã được mở nhưng đảm bảo rằng mọi ngoại lệ không lan truyền ra khỏi hàm hủy.

Vì vậy, nếu người dùng của một đối tượng tệp muốn xử lý đặc biệt đối với các sự cố liên quan đến việc đóng tệp, họ sẽ gọi thủ công close () và xử lý mọi trường hợp ngoại lệ. Nếu mặt khác họ không quan tâm thì kẻ hủy diệt sẽ bị bỏ lại để xử lý tình huống.

Scott Myers có một bài viết xuất sắc về chủ đề này trong cuốn sách "Hiệu quả C ++"

Biên tập:

Rõ ràng cũng có trong "C ++ hiệu quả hơn"
Mục 11: Ngăn chặn các ngoại lệ rời khỏi các hàm hủy


5
"Trừ khi bạn không bận tâm đến khả năng chấm dứt ứng dụng, bạn có thể nên nuốt lỗi." - đây có lẽ nên là ngoại lệ (tha thứ cho trò chơi chữ) chứ không phải là quy tắc - nghĩa là thất bại nhanh.
Erik Forbes

15
Tôi không đồng ý. Kết thúc chương trình dừng ngăn xếp thư giãn. Không có thêm kẻ hủy diệt sẽ được gọi. Bất kỳ tài nguyên mở sẽ được để lại mở. Tôi nghĩ rằng nuốt ngoại lệ sẽ là lựa chọn ưa thích.
Martin York

20
Hệ điều hành acan dọn sạch tài nguyên mà chủ sở hữu tắt. Bộ nhớ, FileHandles, v.v ... Còn tài nguyên phức tạp: Kết nối DB. Đường lên tới ISS mà bạn đã mở (nó có tự động gửi các kết nối chặt chẽ không)? Tôi chắc chắn NASA sẽ muốn bạn đóng kết nối sạch sẽ!
Martin York

7
Nếu một ứng dụng sẽ "thất bại nhanh" bằng cách hủy bỏ, ứng dụng đó không nên ném ngoại lệ ngay từ đầu. Nếu nó sẽ thất bại bằng cách chuyển điều khiển sao lưu stack, nó không nên làm theo cách có thể khiến chương trình bị hủy bỏ. Người này hay người kia, đừng chọn cả hai.
Tom

2
@LokiAstari Giao thức vận chuyển bạn đang sử dụng để liên lạc với tàu vũ trụ không thể xử lý kết nối bị mất? Ok ...
doug65536

54

Ném ra khỏi một hàm hủy có thể dẫn đến một sự cố, bởi vì hàm hủy này có thể được gọi là một phần của "Ngăn xếp ngăn xếp". Giải nén ngăn xếp là một thủ tục diễn ra khi ném ngoại lệ. Trong thủ tục này, tất cả các đối tượng được đẩy vào ngăn xếp kể từ khi "thử" và cho đến khi ngoại lệ được ném, sẽ bị chấm dứt -> hàm hủy của chúng sẽ được gọi. Và trong quy trình này, không được phép ném ngoại lệ khác, vì không thể xử lý hai ngoại lệ cùng một lúc, do đó, điều này sẽ gây ra lệnh gọi hủy bỏ (), chương trình sẽ bị sập và điều khiển sẽ quay trở lại HĐH.


1
bạn có thể giải thích cách abort () được gọi trong tình huống trên không. Có nghĩa là quyền kiểm soát thực thi vẫn còn với trình biên dịch C ++
Krishna Oza

1
@Krishna_Oza: Khá đơn giản: bất cứ khi nào có lỗi xảy ra, mã phát sinh lỗi sẽ kiểm tra một số bit cho biết hệ thống thời gian chạy đang trong quá trình giải nén ngăn xếp (nghĩa là xử lý một số khác thrownhưng chưa tìm thấy catchkhối nào cho nó) trong trường hợp này std::terminate(không abort) được gọi thay vì đưa ra một ngoại lệ (mới) (hoặc tiếp tục giải nén ngăn xếp).
Marc van Leeuwen

53

Chúng ta phải phân biệt ở đây thay vì mù quáng làm theo lời khuyên chung cho các trường hợp cụ thể .

Lưu ý rằng những điều sau đây bỏ qua vấn đề về các thùng chứa các đối tượng và phải làm gì khi đối mặt với nhiều yếu tố của các đối tượng bên trong các container. (Và nó có thể bị bỏ qua một phần, vì một số đối tượng không phù hợp để đưa vào thùng chứa.)

Toàn bộ vấn đề trở nên dễ dàng hơn khi chúng ta chia các lớp thành hai loại. Một dtor lớp có thể có hai trách nhiệm khác nhau:

  • (R) phát hành ngữ nghĩa (còn gọi là bộ nhớ miễn phí)
  • (C) cam kết ngữ nghĩa (còn gọi là xóa tập tin vào đĩa)

Nếu chúng ta xem câu hỏi theo cách này, thì tôi nghĩ rằng có thể lập luận rằng ngữ nghĩa (R) không bao giờ gây ra ngoại lệ từ một dtor vì có một) chúng ta không thể làm gì về nó và b) nhiều hoạt động tài nguyên miễn phí không thậm chí cung cấp để kiểm tra lỗi, ví dụ .void free(void* p);

Các đối tượng có ngữ nghĩa (C), giống như một đối tượng tệp cần xóa thành công dữ liệu của nó hoặc kết nối cơ sở dữ liệu ("được bảo vệ phạm vi") mà một cam kết trong dtor thuộc loại khác: Chúng ta có thể làm gì đó về lỗi (trên cấp độ ứng dụng) và chúng tôi thực sự không nên tiếp tục như không có gì xảy ra.

Nếu chúng ta đi theo lộ trình RAII và cho phép các đối tượng có ngữ nghĩa (C) trong các công cụ của họ, tôi nghĩ rằng chúng ta cũng phải cho phép trong trường hợp kỳ lạ mà các công tử đó có thể ném. Theo sau đó, bạn không nên đặt các đối tượng như vậy vào các thùng chứa và nó cũng theo sau rằng chương trình vẫn có thể terminate()nếu một máy chủ cam kết ném trong khi một ngoại lệ khác đang hoạt động.


Liên quan đến việc xử lý lỗi (ngữ nghĩa Cam kết / Phục hồi) và các trường hợp ngoại lệ, có một bài nói hay của một Andrei Alexandrescu : Xử lý lỗi trong C ++ / Luồng kiểm soát khai báo (tổ chức tại NDC 2014 )

Trong các chi tiết, anh giải thích cách thư viện Folly thực hiện một công cụ UncaughtExceptionCountercho họ ScopeGuard.

(Tôi nên lưu ý rằng những người khác cũng có ý tưởng tương tự.)

Mặc dù cuộc nói chuyện không tập trung vào việc ném từ một vị thần, nhưng nó cho thấy một công cụ có thể được sử dụng ngày nay để loại bỏ các vấn đề khi nào nên ném từ một vị thần.

Trong tương lai , có thể là một tính năng tiêu chuẩn cho điều này, thấy N3614 , và một cuộc thảo luận về nó .

Cập nhật '17: Tính năng 17 std::uncaught_exceptionsCd của C ++ cho điều này là afaikt. Tôi sẽ nhanh chóng trích dẫn bài viết cppref:

Ghi chú

Một ví dụ trong đó việc intquay lại uncaught_exceptionsđược sử dụng là ... ... trước tiên tạo một đối tượng bảo vệ và ghi lại số lượng ngoại lệ chưa được lưu trong hàm tạo của nó. Đầu ra được thực hiện bởi hàm hủy của đối tượng bảo vệ trừ khi foo () ném ( trong trường hợp đó, số lượng ngoại lệ chưa được lưu trong hàm hủy lớn hơn so với những gì hàm tạo quan sát được )


6
Rất đồng ý. Và thêm một ngữ nghĩa rollback ngữ nghĩa (Ro) nữa. Được sử dụng phổ biến trong phạm vi bảo vệ. Giống như trường hợp trong dự án của tôi, nơi tôi đã xác định macro ON_SCOPE_EXIT. Trường hợp về ngữ nghĩa rollback là bất cứ điều gì có ý nghĩa có thể xảy ra ở đây. Vì vậy, chúng tôi thực sự không nên bỏ qua thất bại.
Weipeng L

Tôi cảm thấy như lý do duy nhất chúng tôi cam kết ngữ nghĩa trong các hàm hủy là C ++ không hỗ trợ finally.
dùng541686

@Mehrdad: finally một dtor. Nó luôn được gọi, không có vấn đề gì. Để biết gần đúng cú pháp cuối cùng, hãy xem các triển khai scope_guard khác nhau. Ngày nay, với máy móc tại chỗ (ngay cả trong tiêu chuẩn, có phải là C ++ 14?) Để phát hiện xem dtor có được phép ném hay không, nó thậm chí có thể được làm cho hoàn toàn an toàn.
Martin Ba

1
@MartinBa: Tôi nghĩ rằng bạn đã bỏ lỡ quan điểm nhận xét của tôi, điều này thật đáng ngạc nhiên vì tôi đã đồng ý với quan điểm của bạn rằng (R) và (C) là khác nhau. Tôi đã cố gắng nói rằng một dtor vốn dĩ là một công cụ cho (R) và finallyvốn dĩ là một công cụ cho (C). Nếu bạn không thấy lý do tại sao: hãy xem xét lý do tại sao việc ném các ngoại lệ lên nhau theo finallykhối là hợp pháp và tại sao điều tương tự lại không dành cho kẻ hủy diệt. (Theo một cách hiểu nào đó, đó là dữ liệu so với điều khiển . Công cụ phá hủy là để phát hành dữ liệu, finallylà để giải phóng quyền kiểm soát. Chúng khác nhau; thật không may khi C ++ liên kết chúng lại với nhau.)
user541686

1
@Mehrdad: Bắt quá lâu ở đây. Nếu bạn muốn, bạn có thể xây dựng các đối số của mình tại đây: lập trình viên.stackexchange.com / questions / 304067 / . Cảm ơn.
Martin Ba

21

Câu hỏi thực sự để tự hỏi về việc ném từ một kẻ hủy diệt là "Người gọi có thể làm gì với điều này?" Có thực sự có bất cứ điều gì hữu ích mà bạn có thể làm với ngoại lệ, điều đó sẽ bù đắp những nguy hiểm được tạo ra bằng cách ném từ một kẻ hủy diệt không?

Nếu tôi phá hủy một Foođối tượng và kẻ Foohủy diệt ném ra một ngoại lệ, tôi có thể làm gì hợp lý với nó? Tôi có thể đăng nhập nó, hoặc tôi có thể bỏ qua nó. Đó là tất cả. Tôi không thể "sửa" nó, vì Foođối tượng đã biến mất. Trường hợp tốt nhất, tôi đăng nhập ngoại lệ và tiếp tục như không có gì xảy ra (hoặc chấm dứt chương trình). Đó có thực sự có giá trị gây ra hành vi không xác định bằng cách ném từ một kẻ hủy diệt?


11
Chỉ cần chú ý ... ném từ một dtor không bao giờ là hành vi không xác định. Chắc chắn, nó có thể gọi chấm dứt (), nhưng đó là hành vi được chỉ định rất tốt.
Martin Ba

4
std::ofstreamHàm hủy của nó tuôn ra và sau đó đóng tệp. Một lỗi đầy đĩa có thể xảy ra trong khi xả, bạn hoàn toàn có thể làm điều gì đó hữu ích với: hiển thị cho người dùng hộp thoại báo lỗi cho biết đĩa hết dung lượng trống.
Andy

13

Nó nguy hiểm, nhưng nó cũng không có ý nghĩa từ quan điểm dễ đọc / dễ hiểu mã.

Những gì bạn phải hỏi là trong tình huống này

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

Điều gì nên bắt ngoại lệ? Người gọi của foo có nên? Hoặc foo nên xử lý nó? Tại sao người gọi của foo quan tâm đến một số đối tượng nội bộ để foo? Có thể có một cách ngôn ngữ định nghĩa điều này có ý nghĩa, nhưng nó sẽ không thể đọc được và khó hiểu.

Quan trọng hơn, bộ nhớ cho Object đi đâu? Bộ nhớ mà đối tượng sở hữu đi đâu? Nó vẫn được phân bổ (bề ngoài vì bộ hủy không thành công)? Cũng xem xét đối tượng đã ở trong không gian ngăn xếp , vì vậy rõ ràng nó đã biến mất bất kể.

Sau đó xem xét trường hợp này

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

Khi xóa obj3 không thành công, làm thế nào để tôi thực sự xóa theo cách được đảm bảo để không thất bại? Đó là ký ức của tôi chết tiệt!

Bây giờ hãy xem xét trong đoạn mã đầu tiên Object sẽ tự động biến mất vì nó nằm trên ngăn xếp trong khi Object3 nằm trên heap. Vì con trỏ tới Object3 không còn nữa, bạn thuộc loại SOL. Bạn bị rò rỉ bộ nhớ.

Bây giờ một cách an toàn để làm mọi việc là sau đây

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

};

Cũng xem Câu hỏi thường gặp này


Làm sống lại câu trả lời này, ví dụ: ví dụ đầu tiên, về int foo(), bạn có thể sử dụng một khối chức năng thử để bọc toàn bộ chức năng foo trong một khối bắt thử, bao gồm cả việc bắt các hàm hủy, nếu bạn quan tâm để làm như vậy. Vẫn không phải là cách tiếp cận ưa thích, nhưng nó là một điều.
tyree731

13

Từ dự thảo ISO cho C ++ (ISO / IEC JTC 1 / SC 22 N 4411)

Vì vậy, các hàm hủy thường nhìn chung các ngoại lệ và không để chúng lan truyền ra khỏi hàm hủy.

3 Quá trình gọi các hàm hủy cho các đối tượng tự động được xây dựng trên đường dẫn từ khối thử đến biểu thức ném được gọi là ngăn xếp ngăn xếp. [Lưu ý: Nếu một hàm hủy được gọi trong khi ngăn xếp ngăn xếp thoát ra với một ngoại lệ, std :: terminating được gọi là (15.5.1). Vì vậy, các hàm hủy thường nhìn chung các ngoại lệ và không để chúng lan truyền ra khỏi hàm hủy. - lưu ý cuối]


1
Không trả lời câu hỏi - OP đã nhận thức được điều này.
Arafangion

2
@Arafangion Tôi nghi ngờ rằng anh ấy đã nhận thức được điều này (std :: chấm dứt được gọi) vì câu trả lời được chấp nhận đưa ra chính xác cùng một điểm.
lothar

@Arafangion như trong một số câu trả lời ở đây, một số người đã đề cập rằng abort () đang được gọi; Hoặc là std :: terminating lần lượt gọi hàm abort ().
Krishna Oza

7

Kẻ hủy diệt của bạn có thể đang thực thi bên trong một chuỗi các hàm hủy khác. Ném một ngoại lệ mà người gọi ngay lập tức của bạn không bắt được có thể khiến nhiều đối tượng ở trạng thái không nhất quán, do đó gây ra nhiều vấn đề hơn sau đó bỏ qua lỗi trong hoạt động dọn dẹp.


7

Tôi thuộc nhóm xem xét rằng mô hình "bảo vệ có phạm vi" ném vào hàm hủy có ích trong nhiều tình huống - đặc biệt đối với các bài kiểm tra đơn vị. Tuy nhiên, hãy lưu ý rằng trong C ++ 11, việc ném vào hàm hủy sẽ dẫn đến một cuộc gọi đến std::terminatevì các hàm hủy được chú thích ngầm với noexcept.

Andrzej Krzemieński có một bài viết tuyệt vời về chủ đề kẻ hủy diệt ném:

Ông chỉ ra rằng C ++ 11 có cơ chế ghi đè mặc định noexceptcho các hàm hủy:

Trong C ++ 11, một hàm hủy được chỉ định ngầm định là noexcept. Ngay cả khi bạn thêm không có đặc điểm kỹ thuật và xác định hàm hủy của bạn như thế này:

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

Trình biên dịch vẫn sẽ vô hình thêm đặc tả noexceptvào hàm hủy của bạn. Và điều này có nghĩa là khoảnh khắc kẻ hủy diệt của bạn ném một ngoại lệ, std::terminatesẽ được gọi, ngay cả khi không có tình huống ngoại lệ kép. Nếu bạn thực sự quyết tâm cho phép kẻ hủy diệt của bạn ném, bạn sẽ phải xác định rõ điều này; bạn có ba lựa chọn:

  • Hoàn toàn chỉ định hàm hủy của bạn là noexcept(false),
  • Kế thừa lớp của bạn từ một lớp khác đã chỉ định hàm hủy của nó là noexcept(false).
  • Đặt một thành viên dữ liệu không tĩnh trong lớp của bạn đã chỉ định hàm hủy của nó là noexcept(false).

Cuối cùng, nếu bạn quyết định ném vào hàm hủy, bạn phải luôn nhận thức được nguy cơ của ngoại lệ kép (ném trong khi ngăn xếp đang được thư giãn vì một ngoại lệ). Điều này sẽ gây ra một cuộc gọi đến std::terminatevà nó hiếm khi là những gì bạn muốn. Để tránh hành vi này, bạn chỉ cần kiểm tra xem đã có ngoại lệ hay chưa trước khi sử dụng một hành vi mới std::uncaught_exception().


6

Mọi người khác đã giải thích tại sao ném chất hủy diệt là khủng khiếp ... bạn có thể làm gì về nó? Nếu bạn đang thực hiện một thao tác có thể thất bại, hãy tạo một phương thức công khai riêng biệt để thực hiện dọn dẹp và có thể đưa ra các ngoại lệ tùy ý. Trong hầu hết các trường hợp, người dùng sẽ bỏ qua điều đó. Nếu người dùng muốn theo dõi sự thành công / thất bại của việc dọn dẹp, họ có thể chỉ cần gọi thói quen dọn dẹp rõ ràng.

Ví dụ:

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

Tôi đang tìm kiếm một giải pháp nhưng họ đang cố gắng giải thích những gì đã xảy ra và tại sao. Chỉ muốn làm cho nó rõ ràng là chức năng đóng được gọi bên trong hàm hủy?
Jason Liu

5

Ngoài các câu trả lời chính, tốt, toàn diện và chính xác, tôi muốn bình luận về bài viết mà bạn tham khảo - bài viết có nội dung "ném ngoại lệ vào kẻ hủy diệt không phải là quá tệ".

Bài viết lấy dòng "các lựa chọn thay thế để ném ngoại lệ" là gì và liệt kê một số vấn đề với từng phương án. Làm như vậy nó kết luận rằng vì chúng ta không thể tìm thấy một giải pháp thay thế không có vấn đề, chúng ta nên tiếp tục ném ngoại lệ.

Vấn đề là không có vấn đề nào mà nó liệt kê với các lựa chọn thay thế là bất kỳ nơi nào gần như tệ như hành vi ngoại lệ, mà, hãy nhớ, là "hành vi không xác định của chương trình của bạn". Một số phản đối của tác giả bao gồm "xấu về mặt thẩm mỹ" và "khuyến khích phong cách xấu". Bây giờ bạn muốn có cái nào hơn? Một chương trình có phong cách xấu, hoặc một chương trình thể hiện hành vi không xác định?


1
Không xác định hành vi, nhưng chấm dứt ngay lập tức.
Marc van Leeuwen

Tiêu chuẩn nói "hành vi không xác định". Hành vi đó thường xuyên chấm dứt nhưng không phải lúc nào cũng vậy.
DJClayworth

Không, đọc [trừ.terminate] trong Xử lý ngoại lệ-> Các hàm đặc biệt (là 15,5.1 trong bản sao tiêu chuẩn của tôi, nhưng việc đánh số của nó có lẽ đã lỗi thời).
Marc van Leeuwen

2

Q: Vì vậy, câu hỏi của tôi là thế này - nếu ném từ một hàm hủy dẫn đến hành vi không xác định, làm thế nào để bạn xử lý các lỗi xảy ra trong một hàm hủy?

A: Có một số lựa chọn:

  1. Hãy để các ngoại lệ chảy ra khỏi kẻ hủy diệt của bạn, bất kể điều gì đang xảy ra ở nơi khác. Và khi làm như vậy, hãy lưu ý (hoặc thậm chí là sợ hãi) rằng std :: terminating có thể theo sau.

  2. Không bao giờ để ngoại lệ chảy ra khỏi hàm hủy của bạn. Có thể được ghi vào một bản ghi, một số văn bản xấu lớn màu đỏ nếu bạn có thể.

  3. fave của tôi : Nếu std::uncaught_exceptiontrả về false, hãy để bạn ngoại lệ chảy ra. Nếu nó trả về true, thì quay lại cách tiếp cận đăng nhập.

Nhưng nó có tốt để ném vào d'tors?

Tôi đồng ý với hầu hết các điều trên rằng ném tốt nhất nên tránh trong hàm hủy, nơi có thể. Nhưng đôi khi bạn tốt nhất nên chấp nhận nó có thể xảy ra và xử lý nó tốt. Tôi muốn chọn 3 ở trên.

Có một vài trường hợp kỳ lạ trong đó thực sự là một ý tưởng tuyệt vời để ném từ một kẻ hủy diệt. Giống như mã lỗi "phải kiểm tra". Đây là một loại giá trị được trả về từ một hàm. Nếu người gọi đọc / kiểm tra mã lỗi được chứa, giá trị trả về sẽ âm thầm phá hủy. Nhưng , nếu mã lỗi được trả về chưa được đọc vào thời điểm các giá trị trả về nằm ngoài phạm vi, nó sẽ ném một số ngoại lệ, từ hàm hủy của nó .


4
Fave của bạn là thứ tôi đã thử gần đây, và hóa ra bạn không nên làm điều đó. gotw.ca/gotw/047.htm
GManNickG 18/03/2016

1

Tôi hiện đang tuân theo chính sách (mà rất nhiều người đang nói) rằng các lớp không nên chủ động ném ngoại lệ khỏi các hàm hủy của chúng mà thay vào đó nên cung cấp một phương thức "đóng" công khai để thực hiện thao tác có thể thất bại ...

... nhưng tôi tin rằng các hàm hủy cho các lớp kiểu container, như một vectơ, không nên che dấu các ngoại lệ được ném từ các lớp mà chúng chứa. Trong trường hợp này, tôi thực sự sử dụng phương pháp "miễn phí / đóng" tự gọi đệ quy. Vâng, tôi nói đệ quy. Có một phương pháp cho sự điên rồ này. Việc truyền ngoại lệ phụ thuộc vào việc có một ngăn xếp: Nếu xảy ra một ngoại lệ duy nhất, thì cả hai hàm hủy còn lại sẽ vẫn chạy và ngoại lệ đang chờ xử lý sẽ lan truyền một khi thường trình trở lại, điều này thật tuyệt. Nếu nhiều trường hợp ngoại lệ xảy ra, thì (tùy thuộc vào trình biên dịch), ngoại lệ đầu tiên đó sẽ lan truyền hoặc chương trình sẽ chấm dứt, điều đó không sao cả. Nếu có quá nhiều trường hợp ngoại lệ xảy ra rằng đệ quy tràn ra ngăn xếp thì có gì đó sai nghiêm trọng và ai đó sẽ tìm hiểu về nó, điều đó cũng không sao. Cá nhân,

Vấn đề là container vẫn trung lập, và tùy thuộc vào các lớp được chứa để quyết định xem họ có hành xử hay hành vi sai trái liên quan đến việc ném ngoại lệ từ kẻ hủy diệt của họ hay không.


1

Không giống như các nhà xây dựng, nơi ném ngoại lệ có thể là một cách hữu ích để chỉ ra rằng việc tạo đối tượng đã thành công, các ngoại lệ không nên được ném vào các hàm hủy.

Sự cố xảy ra khi một ngoại lệ được ném ra từ một hàm hủy trong quá trình giải nén ngăn xếp. Nếu điều đó xảy ra, trình biên dịch sẽ rơi vào tình huống không biết nên tiếp tục quá trình giải nén ngăn xếp hay xử lý ngoại lệ mới. Kết quả cuối cùng là chương trình của bạn sẽ bị chấm dứt ngay lập tức.

Do đó, cách hành động tốt nhất chỉ là tránh sử dụng ngoại lệ trong các công cụ hủy diệt hoàn toàn. Thay vào đó hãy viết một tin nhắn vào một tệp nhật ký.


1
Viết một tin nhắn vào tệp nhật ký có thể gây ra một ngoại lệ.
Konard

1

Martin Ba (ở trên) đang đi đúng hướng - kiến ​​trúc sư của bạn khác nhau về logic ĐÁNG TIN CẬY và CAM KẾT.

Để phát hành:

Bạn nên ăn bất kỳ lỗi. Bạn đang giải phóng bộ nhớ, đóng kết nối, v.v. Không ai khác trong hệ thống sẽ TÌM lại những thứ đó và bạn đang trao lại tài nguyên cho HĐH. Nếu có vẻ như bạn cần xử lý lỗi thực sự ở đây, thì đó có thể là hậu quả của lỗi thiết kế trong mô hình đối tượng của bạn.

Đối với Cam kết:

Đây là nơi bạn muốn cùng loại đối tượng trình bao bọc RAII mà những thứ như std :: lock_guard đang cung cấp cho mutexes. Với những người bạn không đặt logic cam kết trong dtor AT ALL. Bạn có một API dành riêng cho nó, sau đó các đối tượng trình bao bọc sẽ RAII cam kết nó trong các bộ xử lý THEIR và xử lý các lỗi ở đó. Hãy nhớ rằng, bạn có thể CATCH ngoại lệ trong một hàm hủy tốt; nó ban hành chúng thật nguy hiểm Điều này cũng cho phép bạn thực hiện chính sách và xử lý lỗi khác nhau chỉ bằng cách xây dựng một trình bao bọc khác (ví dụ: std :: unique_lock so với std :: lock_guard) và đảm bảo bạn sẽ không quên gọi logic cam kết - đó là một nửa cách duy nhất biện minh tốt cho việc đặt nó trong một dtor ở vị trí số 1.


1

Vì vậy, câu hỏi của tôi là thế này - nếu ném từ một hàm hủy dẫn đến hành vi không xác định, làm thế nào để bạn xử lý các lỗi xảy ra trong một hàm hủy?

Vấn đề chính là đây: bạn không thể thất bại . Rốt cuộc có nghĩa là gì khi thất bại? Nếu việc giao dịch với cơ sở dữ liệu không thành công và thất bại (không thể khôi phục), điều gì xảy ra với tính toàn vẹn của dữ liệu của chúng tôi?

Vì các hàm hủy được gọi cho cả hai đường thông thường và ngoại lệ (thất bại), bản thân chúng không thể thất bại nếu không chúng ta "thất bại".

Đây là một vấn đề khó khăn về mặt khái niệm nhưng thường giải pháp là chỉ cần tìm một cách để đảm bảo rằng thất bại không thể thất bại. Ví dụ: cơ sở dữ liệu có thể ghi các thay đổi trước khi cam kết cấu trúc dữ liệu bên ngoài hoặc tệp. Nếu giao dịch thất bại, thì cấu trúc tệp / dữ liệu có thể bị loại bỏ. Tất cả những gì phải đảm bảo là cam kết những thay đổi từ cấu trúc bên ngoài đó / gửi một giao dịch nguyên tử không thể thất bại.

Giải pháp thực tế có lẽ chỉ cần đảm bảo rằng cơ hội thất bại trong thất bại là không thể thực hiện được, vì làm cho mọi thứ không thể thất bại có thể gần như không thể trong một số trường hợp.

Giải pháp phù hợp nhất với tôi là viết logic không dọn dẹp của bạn theo cách mà logic dọn dẹp không thể thất bại. Ví dụ: nếu bạn muốn tạo cấu trúc dữ liệu mới để dọn sạch cấu trúc dữ liệu hiện có, thì có lẽ bạn có thể tìm cách tạo cấu trúc phụ trợ đó trước để chúng ta không còn phải tạo cấu trúc bên trong hàm hủy.

Điều này nói thì dễ hơn nhiều so với thực hiện, nhưng đó là cách thực sự đúng đắn duy nhất tôi thấy để nói về nó. Đôi khi tôi nghĩ rằng nên có khả năng viết logic hủy diệt riêng biệt cho các đường dẫn thực thi thông thường khỏi các đường dẫn ngoại lệ, vì đôi khi các hàm hủy có cảm giác hơi giống như chúng có trách nhiệm gấp đôi bằng cách cố gắng xử lý cả hai (một ví dụ là các bộ bảo vệ phạm vi yêu cầu loại bỏ rõ ràng ; họ sẽ không yêu cầu điều này nếu họ có thể phân biệt các con đường phá hủy đặc biệt với những con đường không đặc biệt).

Vấn đề cuối cùng là chúng ta không thể thất bại và đó là một vấn đề thiết kế khái niệm khó giải quyết trong mọi trường hợp. Sẽ dễ dàng hơn nếu bạn không bị cuốn vào các cấu trúc điều khiển phức tạp với hàng tấn vật thể tuổi teen tương tác với nhau, và thay vào đó mô hình hóa các thiết kế của bạn theo kiểu hơi cồng kềnh (ví dụ: hệ thống hạt với bộ phá hủy để phá hủy toàn bộ hạt hệ thống, không phải là một hàm hủy không tầm thường riêng biệt cho mỗi hạt). Khi bạn mô hình hóa các thiết kế của mình ở mức độ thô hơn này, bạn sẽ có ít hơn các công cụ phá hủy không tầm thường để xử lý, và cũng có thể thường xuyên chi trả cho bất kỳ bộ nhớ / xử lý nào được yêu cầu để đảm bảo rằng các công cụ phá hủy của bạn không thể thất bại.

Và đó là một trong những giải pháp đơn giản nhất một cách tự nhiên là sử dụng các công cụ phá hủy ít thường xuyên hơn. Trong ví dụ về hạt ở trên, có lẽ khi phá hủy / loại bỏ hạt, một số điều nên được thực hiện có thể thất bại vì bất kỳ lý do gì. Trong trường hợp đó, thay vì gọi logic như vậy thông qua dtor của hạt có thể được thực thi trong một đường dẫn đặc biệt, thay vào đó bạn có thể thực hiện tất cả bởi hệ thống hạt khi loại bỏ hạt. Loại bỏ một hạt có thể luôn luôn được thực hiện trong một con đường không đặc biệt. Nếu hệ thống bị phá hủy, có thể nó chỉ có thể thanh lọc tất cả các hạt và không bận tâm với logic loại bỏ hạt riêng lẻ đó có thể bị lỗi, trong khi logic có thể bị hỏng chỉ được thực thi trong quá trình thực thi bình thường của hệ thống hạt khi loại bỏ một hoặc nhiều hạt.

Thường có những giải pháp như thế sẽ mọc lên nếu bạn tránh phải đối phó với nhiều đối tượng tuổi teen với những kẻ hủy diệt không tầm thường. Nơi bạn có thể bị vướng vào một mớ hỗn độn, nơi dường như không thể là ngoại lệ - an toàn là khi bạn bị vướng vào rất nhiều đồ vật tuổi teen mà tất cả đều có những kẻ không tầm thường.

Nó sẽ giúp ích rất nhiều nếu nothrow / noexcept thực sự được dịch thành lỗi trình biên dịch nếu bất cứ thứ gì chỉ định nó (bao gồm các hàm ảo sẽ kế thừa đặc tả noexcept của lớp cơ sở của nó) đã cố gắng gọi bất cứ thứ gì có thể ném. Bằng cách này, chúng ta sẽ có thể bắt được tất cả những thứ này vào thời gian biên dịch nếu chúng ta thực sự viết một hàm hủy vô tình có thể ném.


1
Phá hủy là thất bại bây giờ?
tò mò

Tôi nghĩ rằng anh ta có nghĩa là kẻ hủy diệt được gọi trong một thất bại, để dọn sạch sự thất bại đó. Vì vậy, nếu một hàm hủy được gọi trong một ngoại lệ hoạt động, thì nó không thể dọn sạch khỏi một lỗi trước đó.
dùng2445507

0

Đặt một sự kiện báo động. Thông thường các sự kiện báo động là hình thức tốt hơn để thông báo sự cố trong khi làm sạch các đối tượng

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.