2010-12-19 53 views
10

我只是一個初出茅廬的程序員,至少試圖編程比最好的情況更多。我一直在閱讀Herb Sutter的「Exceptional C++」,並且迄今爲止已經進行了三次異常安全章節的討論。然而,除了他提出的例子(一個Stack)之外,我不確定什麼時候我應該爭取異常安全與速度,以及什麼時候這很簡單。例外安全 - 何時,如何,爲什麼?

例如,我目前的家庭作業項目是一個雙向鏈接列表。既然我已經編寫了其中的一些,我想花時間去深入一些更深入的概念,比如ES。

這裏是我的彈出式前功能:

void List::pop_front() 
{ 
    if(!head_) 
     throw std::length_error("Pop front: List is empty.\n"); 
    else 
    { 
     ListElem *temp = head_; 
     head_   = head_->next; 
     head_->prev = 0; 
     delete temp; 
     --size_; 
    } 
} 

我遇到了一些難題與此有關。

1)列表失敗時,我真的應該拋出一個錯誤嗎?我不應該寧願乾脆不做任何事情,而是返回而不是強迫列表的用戶執行try {] catch(){}語句(這也是很慢的)。

2)有多個錯誤類(加上我的老師要求我們在課堂上實現的ListException)。對於這樣的事情是否真的需要一個自定義的錯誤類,並且在何時使用特定的異常類時是否有一個通用指南? (例如,範圍,長度和邊界全部聲音相似)

3)我知道我不應該改變程序狀態,直到所有引發異常的代碼都完成爲止。這就是爲什麼我最後減小尺寸。這個簡單的例子真的有必要嗎?我知道刪除不能扔。分配給0時head_-> prev是否可能拋出? (頭的第一個節點)

我的push_back功能:

void List::push_back(const T& data) 
{ 
    if(!tail_) 
    { 
     tail_ = new ListElem(data, 0, 0); 
     head_ = tail_; 
    } 
    else 
    { 
    tail_->next = new ListElem(data, 0, tail_); 
    tail_ = tail_->next; 
    } 
    ++size_; 
} 

1)我聽到常常是任何可以在C++程序失敗。測試ListElem的構造函數是否失敗(或在new ing期間尾部)是否現實?

2)是否有必要測試數據類型(目前簡單的typedef int T,直到我對所有東西進行模板化)以確保該類型對結構可行?

我意識到這些都是過於簡單的例子,但我現在只是混淆了什麼時候應該實際練習好的ES,以及什麼時候沒有。

+3

我覺得'push_back'有一個bug。如果列表爲空,它會在列表中插入'data'兩次,但只增加一次'size' ... – 2010-12-19 12:14:50

+0

是的,肯定缺少'else'和一些大括號。 ;)我的回答是基於這個假設得到解決的。 – 2010-12-19 12:18:45

+1

+1。這個問題應該重新編制並放入C++ - faq標籤中。大多數人或者不知道安全是什麼例外,或者對安全有誤解,而其他人可能希望記憶更新。 – 2010-12-19 12:50:17

回答

4

這是一個很長的問題。我將採納編號爲1)的所有問題。

1)我應該真的在列表失敗時拋出一個錯誤嗎?我不應該寧願乾脆不做任何事情,而是返回而不是強迫列表的用戶執行try {] catch(){}語句(這也是緩慢的)。

不。如果您的用戶關心性能,他們會在嘗試彈出之前檢查長度,而不是彈出並捕獲異常。這是一個例外,它會告訴用戶他們是否忘記首先檢查長度,並且此時您確實希望應用程序暴露在他們的臉上。如果你什麼都不做,可能會導致稍後纔會顯示的細微問題,這會使調試更加困難。

1)我經常聽說任何事情都可能在C++程序中失敗。測試ListElem的構造函數是否失敗(或在newing期間是tail_)是否現實?

例如,如果內存不足,構造函數可能會失敗,但在這種情況下,它應該引發異常,而不是返回null。所以你不應該明確地測試構造函數失敗。看到這個問題的更多細節:

+0

我想說,提供兩種方法是很好的:一種是錯誤檢查和拋出,另一種是爲了提高速度。這也是'std :: vector'所做的。我總是依靠良好的錯誤檢查,對我來說,它比微觀優化更重要的20倍。當然,我總是選擇一個合適的數據結構。在我看來,大多數C++開發人員和書籍作者都過度關注速度問題。 – 2010-12-19 14:02:57

2

我聽說經常什麼都可以在C++程序失敗。測試ListElem的構造函數是否失敗(或在newing期間是tail_)是否現實?

是的,這是現實。否則,如果程序內存不足並且分配失敗(或者由於某些其他內部原因導致構造器失敗),那麼稍後將遇到問題。

基本上,你必須發信號失敗任何時候,代碼是不能完全做它的API聲明它會做的事情。

唯一的區別是你如何信號故障 - 通過返回值或通過例外。如果存在性能方面的考慮,返回值可能比例外更好。但是這兩種方法在調用者中都需要特殊的錯誤捕獲代碼。

+0

FWIW,'void pop_back()'是標準庫順序容器使用的接口。 – 2010-12-19 12:17:18

1

爲了您的第一組問題:

  1. 是的,你應該拋出,在@馬克的回答全部的原因。 (+1給他)
  2. 這並不是真的必要,但它可以讓你的呼叫者的生活變得更容易。異常處理的好處之一是它將代碼本地化以便在一個地方一起處理特定類別的錯誤。通過拋出一個特定的異常類型,你可以讓你的調用者專門捕獲這個錯誤,或者通過捕獲你拋出的特定異常的超類來更具體地瞭解它。
  3. else中的所有陳述都提供了不保證。

關於你的第二組:

  1. 不,這不是現實的考驗。你不知道底層構造函數可能拋出什麼。這可能是預期的項目(即std::bad_alloc),也可能是一些奇怪的事情(即int),因此你可以處理它的唯一辦法是把它放在一個catch(...)這是邪惡的:)

    在另一個裏面只要在if塊內部創建的虛擬末端節點將被鏈表的析構函數加載,那​​麼現有的方法已經是異常安全的。 (也就是new之後的所有東西都提供了NOTHOW)

  2. 假設T的任何操作都可以拋出,但析構函數除外。

8

我真的應該在列表失敗時拋出一個錯誤嗎?我不應該寧願乾脆不做任何事情,而是返回而不是強迫列表的用戶執行try {] catch(){}語句(這也是緩慢的)。

絕對拋出異常。

用戶必須知道發生了什麼,如果清單是空的 - 否則將是地獄調試。用戶是而不是被迫使用try/catch語句;如果異常是意外的(即只能由於程序員錯誤而發生),那麼沒有理由試圖去捕捉它。當一個異常未被捕獲時,它通過std :: terminate和這是非常有用的行爲。無論如何,try/catch語句本身並不慢,實際拋出異常和解除堆棧的成本是多少。如果異常沒有被拋出,它幾乎沒有花費。

有多個錯誤類(加上我的老師要求我們在課堂上實現的ListException)。對於這樣的事情是否真的需要一個自定義的錯誤類,並且在何時使用特定的異常類時是否有一個通用指南? (例如,範圍,長度和邊界全部聽起來一樣)

儘可能具體。使用你自己的錯誤類是做到這一點的最好方法。使用繼承來分組相關的異常(以便呼叫者可以更容易地捕捉它們)。

我知道我不應該改變程序狀態,直到所有引發異常的代碼都完成爲止。這就是爲什麼我最後減小尺寸。這個簡單的例子真的有必要嗎?我知道刪除不能扔。分配給0時head_-> prev是否可能拋出? (頭部是第一節點)

head_如果爲空,則解引用它(如試圖分配給head_->prev的一部分)是未定義的行爲。拋出一個異常是未定義行爲的一個可能後果,但是不太可能的結果(它要求編譯器不會用它來握住你的手,用一種語言來說,這種事情被認爲是荒謬的);而不是一種我們擔心,因爲未定義的行爲是未定義的行爲 - 這意味着你的程序已經是錯誤的了,並且試圖讓錯誤的方式更加正確沒有意義。

另外,您已經明確檢查head_是否不爲空。所以沒有問題,假設你沒有使用線程進行任何操作。

我經常聽說任何事情都可能在C++程序中失敗。

這有點偏執。 :)

測試ListElem的構造函數是否失敗(或在newing期間tail_)是否現實?

如果new失敗,則會拋出std::bad_alloc的實例。拋出一個異常正是你想要在這裏發生的事情,所以你不想或不需要做任何事情 - 只要讓它傳播。將錯誤重新描述爲某種列表異常並不能真正添加​​有用的信息,並且可能會使事情進一步模糊。

如果構造函數ListElem失敗,則應該通過拋出異常來失敗,並且大約在999到1之間,您應該讓這個失敗。

的關鍵在這裏是每當一個異常被拋出在這裏,有沒有清理工作要做,因爲您沒有修改列表着呢,構建/ newed對象正式根本不存在(TM)。只要確保其構造函數是異常安全的,你就會好起來的。如果new調用未能分配內存,則構造函數甚至不會被調用。

您不用擔心的時間是當您在同一地點多次分配時。在這種情況下,您必須確保如果第二次分配失敗,您會捕獲異常(不管它是什麼),清理第一次分配並重新拋出。否則,你泄漏了第一個分配。

難道永遠是必要的(直到我模板化的一切目前簡單的typedef INT T),以確保該類型是可行的結構測試數據的類型?

類型在編譯時檢查。你無法在運行時真實地對它們做任何事情,也不會實際需要。 (如果你不想要所有的類型檢查,那麼爲什麼你使用的是一種語言,迫使你只能在整個地方輸入類型名稱:))

6

我並不確定何時正是我 應爭取異常安全VS 速度

你應該總是爭取異常安全。請注意,「異常安全性」並不意味着「如果出現任何問題,則拋出異常」。它意味着「提供三種例外保證中的一種:弱,強或不合邏輯」。拋出異常是可選的。爲了讓您的代碼的調用者能夠滿足他們的代碼在發生錯誤時能夠正確運行,必須具有異常安全性。

您將看到來自不同C++程序員/團隊的有關異常的非常不同的樣式。有些人使用它們很多,其他的幾乎沒有(甚至根本不是,儘管我認爲現在很少見)。Google可能是最有名的例子,如果你感興趣的話,可以查看他們的C++風格指南。嵌入式設備和遊戲內部可能是下一個最有可能找到完全用C++來避免異常的例子的地方)。標準的iostreams庫允許你在流上設置一個標誌,當I/O錯誤發生時它們是否應該拋出異常。缺省值是而不是,這對於幾乎所有其他語言的程序員來說都是一個驚喜。

我應該真的在列表失敗時拋出錯誤嗎?

這不是「列表」失敗,具體是pop_front在列表爲空時失敗時調用。你不能概括一個類的所有操作,他們應該總是拋出異常,你必須考慮特定的情況。在這種情況下,您至少有五個合理的選項:

  • 返回值以指示是否有任何內容被彈出。呼叫者可以用它做任何他們喜歡的事情,或者忽略它。
  • 文件說明它是未定義的行爲,當列表爲空時調用pop_front,然後忽略代碼中pop_front的可能性。 UB彈出一個空的標準容器,一些標準庫實現不包含檢查代碼,特別是在發佈版本中。
  • 證明它是未定義的行爲,但仍然執行檢查,並中止程序或拋出異常。您也許可以僅在調試版本中進行檢查(這是assert的作用),在這種情況下,您也可以選擇觸發調試器斷點。
  • 文件說明如果列表爲空,則調用不起作用。
  • 文檔說明列表爲空時拋出異常。

所有這些除了最後一個意味着你的功能可以提供「nothrow」保證。你選擇哪一個取決於你想讓你的API看起來像什麼,以及你想讓你的調用者找到他們的錯誤有什麼樣的幫助。請注意,拋出異常並不會強制您的直接呼叫者來捕捉它。異常只能由能夠從錯誤中恢復的代碼捕獲(或者可選地在程序的最頂端)。

就個人而言,我傾向於而不是拋出用戶錯誤的例外情況,我還傾向於說彈出空列表是用戶錯誤。這並不意味着在調試模式下進行各種檢查是沒有用的,只是我通常不定義API來保證這些檢查將在所有模式下執行。

是對這樣的事情一個自定義錯誤類真的有必要

不,這不是必要,因爲這是一個可以避免的錯誤。在致電pop_front之前,通過檢查列表是否非空,呼叫者可以始終確保它不會被拋出。 std::logic_error將是一個非常合理的例外拋出。使用特殊的異常類的主要原因是,調用者可以捕捉到這種異常:無論您認爲調用者是否需要爲特定情況執行此操作,都取決於您。

當分配給0時head_-> prev是否可能拋出?

除非你的程序以某種方式引發了未定義的行爲。所以是的,你可以在此之前減小大小,並且你可以在delete之前遞減它,前提是你確定ListElem的析構函數不能拋出。在編寫任何析構函數時,應確保它不會拋出。

我經常聽說任何事情都可能在 一個C++程序中失敗。如果ListElem的構造函數失敗 (或tail_在更新期間)測試 是否現實?

這不是真的,一切都會失敗。理想的職能應該記錄他們提供的例外保證,這反過來告訴你他們是否可以投擲。如果他們真的真的記錄良好,他們會列出他們可以拋出的一切,並在什麼情況下扔掉它。

你不應該測試是否new失敗,則應該允許例外的new,如果有的話,從你的函數傳播到您的來電者。然後你可以證明push_front可以拋出std::bad_alloc來指示內存不足,也許它也可以拋出T的拷貝構造函數拋出的任何東西(無論如何,在int的情況下)。您可能不需要爲每個功能分別記錄這些信息 - 有時包含多個功能的一般說明已足夠。它不應該成爲任何人的一個驚喜,如果一個叫做push_front的函數可以拋出,那麼它可以拋出的東西之一是bad_alloc。對於模板容器的用戶來說,它應該也不會讓人驚訝,如果包含的元素拋出異常,那麼這些異常就會被傳播。

難道以往任何時候都需要測試 類型的數據(目前是一個簡單的typedef INT牛逼,直到我模板化 一切),以確保該類型是 可行的結構?

你或許可以編寫你的結構,使得T需要的全部都是可複製構造和賦值的。沒有必要爲此添加特殊測試 - 如果有人試圖用不支持您對其執行的操作的類型實例化模板,則會得到編譯錯誤。不過,你應該記錄下這些要求。