C/C++ Tips - The Marco Assert
Introduction
assert()是個用來debug的macro, 接受一個expression. 如果expression被evalute的結果為整數零, 或能被convert成零(如null pointer), 則會引發assertion failure. Assertion failure會ouput被evluate為零的expression以及其位置, 包括file name, line number(有些compiler也會output function name)至standard error. 緊接著, 發生assertion failure的process會呼叫abort(), 導致process結束(有些compiler會產生core dump). 用以驗證下列precondition:
- Client code對function的操作, 例如給予的參數合不合規範, 或呼叫相關functions的順序是否正確
- Function作者的假設
Assertion Failure Output
假設有個在str_util.c的library function:
size_t string_length (const char* str)
{
// Precondition assertion, client input buffer
// must be a valid string and must not be null!
assert(str != NULL);
// ...
}
當assertion failure發生時, user會於standard error(通常就是terminal)看到類似這樣的訊息:
AppName: str_util.c:245: size_t string_length(): Assertion `str != NULL' failed.這訊息對user(一個programmer)來說通常沒有多大助益, 除非user剛好熟悉這段發生assertion failure的code, 或是能取得發生assertion failure的source code.
假設user能取得這段source code, 有時就算沒有作者的comment, 也應該能合理堆測作者對這function設下了precondition, 此例的precondition為client code所給用以讀取的buffer(str)不能為NULL(當然, 所有precondition最好是寫在說明文件, 若沒文件最起碼也要有comment).
如果你是這library function的作者, 除了做好說明文件以及在source code裡寫足comment, 還有什麼是你能為使用者做的? 讓這使用者在發生assertion failure時更容易修正client code.
More Verbose Output
Imperfect C++(希望沒記錯)提出了一個小技巧, 用在這個地方特別適用. 把上例做點小小修改:
size_t string_length (const char* str)
{
assert("Precondition failed, client supplied "
"input buffer must be a valid string and must "
"not be null" && str != NULL);
// ...
}
幾乎只是把原來的comment搬到assert的expression中, 就能在assertion failure發生時產生更有用的output:
AppName: str_util.c:245: size_t string_length(): Assertion `"Precondition failed, client supplied input buffer must be a valid string and must not be null" && str != NULL' failed.這樣的訊息對user來說明顯的有用多了, user可以清楚知道client code給了string_length()一個null pointer, 而這違反了該function的precondition. User不需要取得該function的source code(甚至不用參考說明文件)也能知道如何修正這個錯誤.
Macro Side-Effect
Assert是個macro, 即使是經驗老到的人, 有時也會忽略掉若是以-DNDEBUG或是M$VC所謂的Relase Mode編譯, assert不但不會產生任何效果, 給assert的expression更是不會被evaluate. 例:
int foo ();
void bar ()
{
assert(foo());
}
以-DNDEBUG編譯, foo()不會被呼叫.
Macro Side-Effect, C++ Specific
C++ Standard Library繼承了絕大部份(也許是全部, 但我不確定)C Standard Library. 只是在include standard header時, 由原來的:#include <assert.h>變成:
#include <cassert>而且會把在該header中屬於global namespace的declartion, 移到std這個namespace當中. 但由於macro(errno也是macro)是preprocessor直接替換的, 與namespace無關(compiler才懂namespace). 因此使用起來像是存在於所有namespace當中(這也是盡量別用macro的原因之一).
Scenarios Not to Use Assert
有些情況, 寫library的人會錯誤地使用assert, 如:
class SomeClass;
void faux (SomeClass* ptr)
{
assert(ptr != NULL);
// ...
}
void foobar (int n)
{
assert(n >= 0);
// ...
}
如果你的code看起來像上例的話, 請像下面這樣, 把interface改一改吧:
void faux (SomeClass& instance)
{
// ...
}
void foobar (unsigned int n)
{
// ...
}


