COdE fr3@K

4/02/2006

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)
    {
      // ...
    }