2012年9月26日 星期三

C++ python-like debug message


在C++下,如果程式寫得大一點,又有自己寫的資料結構、繼承、多型之類的,如果有地方寫錯了,這時debug起來通常相當麻煩;過去筆者都是用gdb跑到當掉的地方,再用backtrace幾次,看看是哪裡出了問題,可是這樣還是有點麻煩;又次用到srilm等套件,backtrace 10次還是不知道錯在哪...
最近跟柏翰學長學到一個debug的技巧(雖然說學長表示他也是跟別人學的),可以在C++上實現python-like的錯誤訊息,很清楚的就能知道錯誤是在哪發生的,在這裡把這個方法整理一下,分享給大家。

1. python-like debug message:
python的debug訊息好處在於,在一個function中出錯,它會有自動traceback的功能,一路把呼叫該錯誤部位的function一路吐出來:
例如一系列函數呼叫如下:err1()->err2()->err3(),然後在err3裡raise一個error,console會輸出
Traceback (most recent call last):
  File "./error.py", line 17, in <module>
    err1()
  File "./error.py", line 14, in err1
    err2()
  File "./error.py", line 11, in err2
    raise(KeyError)
KeyError
其印出的格式整理如下,含$為變數:
File $filename, line $lineno, in $functionname
error code

2. C++ exception:
C++ 提供了try, throw, catch來進行例外的處理機制,相關文件請眾位參考網路上的其他資源,例如:
http://caterpillar.onlyfun.net/Gossip/CppGossip/ClassTemplate.html
這裡只簡單帶過,簡而言之,在try裡造成發生錯誤的情況,throw出一個錯誤,由catch端決定如何處理。
我們利用這樣的處理機制,遇到出現錯誤的狀況,例如提取不存在的資料時,直接throw出一個錯誤,並利用全域的catch(...)將它接下來,利用compiler的內定巨集__FILE__, __LINE__可以編寫出適當的錯誤訊息;同時,可以在catch中再throw一次,將錯誤再往上一層函數丟,如此一來就能達到function traceback的功能,整體程式碼的架構如下:
function err1(parameter)  //throw error function
{
#ifndef NDEBUG
 try{
#endif
 /* code */
#ifndef NDEBUG
 } catch(...) {
 cout << "Traceback:\n";
 cout << "File: " << __FILE__ << ", line "<< __LINE__ << ", in " << __func__ << endl;
 throw; 
 }
#endif
}
利用#ifndef NDEBUG,可以快速重新編譯沒有這些debug訊息的發行版。
利用巨集,則可以讓程式碼看起來更簡潔一點
#define STARTEXCEPTION try{
#define ENDEXCEPTION \
 } catch(...) {\
 cout << "Traceback:\n";\
 cout << "File: " << __FILE__ << ", line "<< __LINE__ << ", in " << __func__ << endl;\
 throw; \
 }

function err1(parameter)  //throw error function
{
#ifndef NDEBUG
STARTEXCEPTION
#endif
 /* code */
#ifndef NDEBUG
ENDEXCEPTION
#endif
}

3. 範例:
我試著提出一個小小的範例,我們現在寫一個Array的object,可以在宣告時指定大小,get 和set 其中的內容(好吧我承認這個code其實就是cv上面連結來的,不過在template的class時我遇到一個template的問題,透過google大神解決了,之後再來寫一篇。)
在get和set的地方,原本都會去檢查index有沒有在範圍之中,否則會存取不存在的值,那我們現在在code中加上上述的exception,下文中,粗體為加入的部分。
template<class CType>
CType Array<CType>::get(const int &i)
{
#ifndef NDEBUG
STARTEXCEPTION
#endif
 if (isSafe(i)) {
  return _Array[i];
 } else {
  cout << "Index out of range\n";
  throw 1;
 }
#ifndef NDEBUG
ENDEXCEPTION
#endif
}
在main裡面,當然也要對呼叫function的地方加上STARTEXCEPTION跟ENDEXCEPTION,當我試圖get超範圍的值時,就會出現錯誤。
Index out of range
Traceback:
File: array.h, line 59, in get
Traceback:
File: main.cpp, line 31, in main
terminate()
我認為,這個方法雖然提供了明確問題的發生途徑,但仍有下列三個問題:
正常的exception做法,應該會去自定義exception handling的class對exception進行處理,才能真正的避免問題,如記憶體漏失;這個方法只是要讓程式終結,輸出較明確的錯誤訊息,而非解決該excetion的發生。
另外,function中使用的__LINE__的行數是大略值,__func__的巨集好像也還沒統一,隨不同家的編譯器也會有不同的定義:
http://stackoverflow.com/questions/2192680/macro-keyword-which-can-be-used-to-print-out-method-name
最後,這個方法深入所有的function,這在程式用到大量函式庫的狀況下,把每個function加上這個code的代價相當昂貴。

4. 如何佈署?
還有另一個問題是,要如何將這些內容佈署到code當中?
在這裡有兩個不同的方式:
第一種是學長所用,取代"^{","^}"為STARTEXCEPTION和ENDEXCEPTION,這個方法極度要求格式的一致,function的括號一定要在行首,像我code格式都亂七八糟的人就不適合。
第二種是用vim的snipmate,定義STARTEXCEPTION和ENDEXCEPTION的pattern ,在指定的地方插入,搜尋時可以利用vim plugin: taglist,可以幫你列出所有的function,並支援快速跳到function 的所在地。

其實我認為應該還有更好的方法,或許是利用cscope或ctags來parse出所有function 的所在地,自動或詢問要不要加上debug訊息,不過目前還沒找到這樣的方法。

5. 結論:
本方法利用C++的exception, throw的功能和g++的MACRO,實現python-like的錯誤訊息,可以有效加速C++複雜程式上的debug過程;利用vim的自定義巨集,我們可以自動在所有函數加入此功能,並用ifndef NDEBUG guard的方式,確保未來可輕易在發行版中移除此功能。
但另一方面,此方法其實無法解決問題,僅是幫助指出問題所在對終止程式,真正防止程式出錯或記憶體管理還是要靠完整的exception handling。

6. 致謝:
此文章感謝柏翰學長的指導
並感謝眾位的觀看,不好意思浪費了大家的時間。

沒有留言:

張貼留言