[英]Why doesn't C++ use std::nested_exception to allow throwing from destructor?
從析構函數拋出異常的主要問題是,在析構函數被調用的那一刻,另一個異常可能是“在飛行中”( std::uncaught_exception() == true
),因此在這種情況下做什么並不明顯。 用新的“覆蓋”舊的異常將是處理這種情況的可能方法之一。 但是決定在這種情況下必須調用std::terminate
(或另一個std::terminate_handler
)。
C ++ 11通過std::nested_exception
類引入了嵌套異常功能。 該特征可用於解決上述問題。 舊(未捕獲)異常可能只是嵌套到新異常中(反之亦然?)然后可能拋出嵌套異常。 但是沒有使用這個想法。 在C ++ 11和C ++ 14中仍然會調用std::terminate
。
所以問題。 是否考慮過嵌套異常的想法? 它有什么問題嗎? 是不是在C ++ 17中會改變這種情況?
當您的析構函數作為堆棧展開過程的一部分執行時(當您的對象未作為堆棧展開的一部分創建時),您引用的問題發生在1 ,並且您的析構函數需要發出異常。
那怎么辦? 你有兩個例外。 異常X
是導致堆棧展開的異常。 異常Y
是析構函數想要拋出的異常。 nested_exception
只能包含其中一個 。
所以也許你有異常Y
包含一個nested_exception
(或者只是一個exception_ptr
)。 所以...你如何處理catch
網站?
如果你抓到Y
,並且碰巧有一些嵌入式X
,你怎么得到它? 請記住: exception_ptr
是類型擦除的 ; 除了傳遞它,你唯一可以做的就是重新拋出它。 那么人們應該這樣做:
catch(Y &e)
{
if(e.has_nested())
{
try
{
e.rethrow_nested();
}
catch(X &e2)
{
}
}
}
我沒有看到很多人這樣做。 特別是因為可能存在極大數量的X
-es。
1 :請不要使用std::uncaught_exception() == true
來檢測這種情況。 這是非常有缺陷的。
std::nested exception
有一個用途,只有一個用途(據我所知)。
話雖如此,它太棒了,我在所有程序中都使用嵌套異常,因此花費時間來搜尋晦澀的bug幾乎為零。
這是因為嵌套異常允許您輕松構建在錯誤點生成的完全注釋的調用堆棧,沒有任何運行時開銷,在重新運行期間不需要大量日志記錄(這將改變時序),並且沒有污染程序邏輯和錯誤處理。
例如:
#include <iostream>
#include <exception>
#include <stdexcept>
#include <sstream>
#include <string>
// this function will re-throw the current exception, nested inside a
// new one. If the std::current_exception is derived from logic_error,
// this function will throw a logic_error. Otherwise it will throw a
// runtime_error
// The message of the exception will be composed of the arguments
// context and the variadic arguments args... which may be empty.
// The current exception will be nested inside the new one
// @pre context and args... must support ostream operator <<
template<class Context, class...Args>
void rethrow(Context&& context, Args&&... args)
{
// build an error message
std::ostringstream ss;
ss << context;
auto sep = " : ";
using expand = int[];
void (expand{ 0, ((ss << sep << args), sep = ", ", 0)... });
// figure out what kind of exception is active
try {
std::rethrow_exception(std::current_exception());
}
catch(const std::invalid_argument& e) {
std::throw_with_nested(std::invalid_argument(ss.str()));
}
catch(const std::logic_error& e) {
std::throw_with_nested(std::logic_error(ss.str()));
}
// etc - default to a runtime_error
catch(...) {
std::throw_with_nested(std::runtime_error(ss.str()));
}
}
// unwrap nested exceptions, printing each nested exception to
// std::cerr
void print_exception (const std::exception& e, std::size_t depth = 0) {
std::cerr << "exception: " << std::string(depth, ' ') << e.what() << '\n';
try {
std::rethrow_if_nested(e);
} catch (const std::exception& nested) {
print_exception(nested, depth + 1);
}
}
void really_inner(std::size_t s)
try // function try block
{
if (s > 6) {
throw std::invalid_argument("too long");
}
}
catch(...) {
rethrow(__func__); // rethrow the current exception nested inside a diagnostic
}
void inner(const std::string& s)
try
{
really_inner(s.size());
}
catch(...) {
rethrow(__func__, s); // rethrow the current exception nested inside a diagnostic
}
void outer(const std::string& s)
try
{
auto cpy = s;
cpy.append(s.begin(), s.end());
inner(cpy);
}
catch(...)
{
rethrow(__func__, s); // rethrow the current exception nested inside a diagnostic
}
int main()
{
try {
// program...
outer("xyz");
outer("abcd");
}
catch(std::exception& e)
{
// ... why did my program fail really?
print_exception(e);
}
return 0;
}
預期產量:
exception: outer : abcd
exception: inner : abcdabcd
exception: really_inner
exception: too long
@Xenial擴展行的說明:
void (expand{ 0, ((ss << sep << args), sep = ", ", 0)... });
args是一個參數包。 它代表0個或多個參數(零很重要)。
我們要做的是讓編譯器為我們擴展參數包,同時圍繞它編寫有用的代碼。
讓我們從外面看:
void(...)
- 表示評估某些東西並丟棄結果 - 但要評估它。
expand{ ... };
記住expand
是int []的typedef,這意味着讓我們計算一個整數數組。
0, (...)...;
意味着第一個整數為零 - 請記住,在c ++中定義零長度數組是非法的。 如果args ...代表0參數怎么辦? 這個0確保數組中至少有一個整數。
(ss << sep << args), sep = ", ", 0);
使用逗號運算符按順序計算表達式序列,獲取最后一個表達式的結果。 表達式是:
s << sep << args
- 打印分隔符,后跟流的當前參數
sep = ", "
- 然后使分隔符指向逗號+空格
0
- 結果為0.這是數組中的值。
(xxx params yyy)...
-用於在參數組中的每個參數做一次params
因此:
void (expand{ 0, ((ss << sep << args), sep = ", ", 0)... });
表示“對於params中的每個參數,在打印分隔符后將其打印到ss。然后更新分隔符(以便我們為第一個分隔符設置不同的分隔符)。將所有這些作為初始化假想數組的一部分,然后我們將拋出遠。
嵌套異常只會添加最可能被忽略的有關所發生事件的信息,這是:
拋出異常X,堆棧正在展開,即在“飛行中”異常調用本地對象的析構函數,其中一個對象的析構函數依次拋出異常Y.
通常這意味着清理失敗。
然后,這不是一個可以通過向上報告並讓更高級別的代碼決定使用一些替代方法來實現其目標來彌補的失敗,因為持有清理所需信息的對象已被破壞 ,有了它的信息,但沒有進行清理。 所以它就像一個斷言失敗。 進程狀態可能非常糟糕,打破了代碼的假設。
拋出的析構函數原則上可以是有用的,例如,Andrei曾經提出過關於在塊范圍退出時指示失敗事務的想法。 也就是說,在正常的代碼執行中,未被告知事務成功的本地對象可以從其析構函數中拋出。 在堆棧展開期間它與C ++的異常規則沖突時,這只會成為一個問題,它需要檢測是否可以拋出異常,這似乎是不可能的。 無論如何,析構函數僅用於其自動調用,而不是用於清理rôle。 因此可以得出結論,當前的C ++規則假定了析構函數的清理工作。
真正的問題是從析構函數中拋出是一個邏輯上的謬誤。 這就像定義operator +()來執行乘法。 不應將析構函數用作運行任意代碼的鈎子。 他們的目的是確定性地釋放資源。 根據定義,這絕不能失敗。 其他任何東西都打破了編寫通用代碼所需的假設。
在使用析構函數的鏈接異常進行堆棧展開期間可能發生的問題是嵌套的異常鏈可能太長。 例如,你有1 000 000
元素的std::vector
,每個元素在它的析構函數中拋出一個異常。 讓我們假設std::vector
析構函數將其元素的析構函數中的所有異常收集到單個嵌套異常鏈中。 然后導致的異常甚至可能比原始的std::vector
容器更大。 這可能會導致性能問題,甚至在堆棧展開期間拋出std::bad_alloc
(甚至無法嵌套,因為沒有足夠的內存來執行此操作)或者在程序中的其他無關位置拋出std::bad_alloc
。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.