今天在網上爬文, 無意間學到了 named initializer 這個到 C99 才出現的語法.

enum 的應用例子


先來看一個 enum 的應用例子:

#include <stdio.h>
void main()
{
    int i;
    enum month {JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,DEC};
    for (i=JAN; i<=DEC; i++)
        printf("\n%d",i);
}

問題


再下來的例子是使用 enum 來當作陣列元素的索引, 這個沒什麼大不了, 相信大家都會用:

#define SIZEOF_ARRAY(arr) (sizeof(arr) / sizeof(arr[0]))

enum Color_t {
      WHITE = 0
    , BLACK
    , GOLD
    , PINK
};

void foo()
{
    static const char * const color_name[] = {
          "white"
        , "black"
        , "gold"
        , "pink"
    };

    for (int i = 0; i < SIZEOF_ARRAY(color_name); ++i) {
        printf("[%d] = %s\n", i, color_name[i]);
    }
}

定義了 enum Color_t 和陣列 color_name之後, 我們就可以像這樣子 color_name[GOLD] 取用字串 "gold", 而不必使用 color_name[2] 這種不好維護的寫法.

這一小段 Demo 的程式雖然看起來還算 OK, 但是對於維護人員來說, 它的背後卻隱藏者幾個嚴重的問題:

  1. 作為陣列索引的 enum Color_t 它的數值個數可能和陣列 color_name 大小不一致.
    尤其是, 這二者是分開定義在不同檔案中, 後來因為新增的需求而需要擴充它們的內容時.
  2. enum Color_t 中數值定義的位置 (轉換為定義值) 可能和其對應的元素在陣列 color_name 中排列的位置不一致.
    狀況和上一個差不多, 也是擴充時維護人員疏忽了, 錯置了或者是遺漏了新增加的字串.

簡易解決方案


第一點, 可以加一個小小的防呆來防範人為的疏忽. 在下面的例子中, 我們在 enum Color_t 定義的最後多設一個 enum 值 (_COLOR_CNT), 並將陣列 color_name 的大小設為這個數值, 而不是由 compiler 自動決定陣列的大小.

...
enum Color_t {
      WHITE = 0
    , BLACK
    , GOLD
    , PINK
    , _COLOR_CNT    // 多加這一個
};
...
    static const char * const color_name[_COLOR_CNT] = {    // 用它來指定陣列大小
          "white"
        , "black"
        , "gold"
        , "pink"
    };
...

不過, 利用 _COLOR_CNT 來指定陣列大小的作法會引發另一個問題: 萬一真的發生只擴充了 enum Color_t 的定義, 卻漏了擴充陣列 color_name 的內容, 最後陣列 color_name 的內容中還是會有多出幾個 null pointer. 因此必需:

  • 每次要運用字串指標時檢查它是不是 null pointer.
  • 或者加上 assert()(註一, 註二) 在執行期確定一下陣列內容是否含有 null pointer.

個人並不喜歡這個方法, 覺得這樣反而引發更多使用上的麻煩, 所以也不建議各位使用. 取而代之, 個人覺得直接用 static_assert(), 在編譯時期即確定一下 _COLOR_CNT 和陣列 color_name 的太小是否一致會是比較合適.

也許你會質疑幹嘛多弄出一個 static_assert() 來, 直接用巨集指令 #if 搭配 sizeof() 不就好了嗎?

// 這個寫法是錯的
#if (SIZEOF_ARRAY(color_name) != _COLOR_CNT)
# error Size of array 'color_name' does not match with '_COLOR_CNT'.
#endif

上面這三行是無法成功編譯的. 這是因為 sizeof() 的計算是在 preporcessor 把巨集展開完之後才執行的. 上面這樣的寫法是在展開巨集時就要計算陣列的大小, 故而編譯失敗. 所以只能使用 C++ (或者 C11) 的 static_assert() 來完成這個功能. 如果你的標頭檔 <assert.h> 裡並沒有這個 static_assert(), 我們也可以改用類似下面的巨集 (更完整的作法可以參考最後的連結段落中所列的文件連結).

// 替代 static_assert() 的巨集
#define STATIC_ASSERT(COND,MSG) typedef char \
static_assertion_##MSG[(COND)?1:-1]
...
// 使用範例
STATIC_ASSERT(SIZEOF_ARRAY(color_name) == _COLOR_CNT, XCOLOR);
...

上面的例子中如果 SIZEOF_ARRAY(color_name)_COLOR_CNT 二者大小不一致時會多一個 static_assertion_XCOLOR 下標為 -1 的編譯錯誤.

註一: 需要引入標頭檔 <assert.h>.

註二: 一般我們並不建議使用 assert() (詳細請參考最後的連結段落中所列的文件連結). 原因是它主要的目的是經由執行一些邏輯計算, 協助我們找出程式中是否有不應該出現的數值錯誤. 一般在編譯交付測試的版本釋出的版本時我們會經由定義巨集 NDEBUG 來將程式中所有的 assert() 移除. 但如此可能又造成程式裡原本應該存在的檢查防護用的程式碼一併被移除 (如果你也是用 assert() 來實作他們). 不過這裡的狀況是編譯期就應該發現的靜態錯誤, 所以暫用無防.

第二點呢, 如果是 C99 版以前的 C 則應該都沒有簡單方便的解決方案. C99 則有一個名為 named initializer 的新語法 (也就是這一篇的主角) 可以解決部份問題:

#define SIZEOF_ARRAY(arr) (sizeof(arr) / sizeof(arr[0]))

enum Color_t {
      WHITE = 0
    , BLACK
    , GOLD
    , PINK
};

void foo()
{
    static const char * const color_name[] = {
          [WHITE] = "white"
        , [BLACK] = "black"
        , [GOLD] = "gold"
        , [PINK] = "pink"
    };

    for (int i = 0; i < SIZEOF_ARRAY(color_name); ++i) {
        printf("[%d] = %s\n", i, color_name[i]);
    }
}

各位可以看到和前者的差別在於定義常數 color_name 的元素時, 每一個字串的前面都帶了一個陣列索引, 這樣可以在編譯期就保證有定義的陣列 color_name 元素他的位置一定會和 enum Color_t 中的定義位置一致.

不過大家要注意的是這個語法只有在 C99 才支援, C99 以前的舊版 C 編譯器是不支援的.

C++ 的支援及注意事項


至於這個 named initializer 寫法在 C++ 上到底有沒有支援呢? 標準的說法是不支援. 但是目前 (2018/10/02) 用線上編譯器卻可以順利完成編譯並執行. 經過一番實驗的驗證個人歸納出了 C++ 和 C99 的對支援這種寫法的差異:

  1. C++ 必需依順序定義陣列元素, 不能跳著定義.
  2. C99 可以不依順序定義陣列元素, 只要有定義即可.

二者相同的部份:

  1. 混用 named initializer 和一般 initializer 時: 接在 named initializer 後面的一般 initializer 它的索引值會是 named initializer 的下一個 (也就是說套用的是原先的規則).
  2. 不會 (也無法) 強制陣列元素的定義個數一定要符合 enum 中的定義 (其實這個很合理).
    因此我們還是需要再加上一行 static_alert();.

總結起來, C++ 套用的規則似乎是好一些.

另一個不應該是問題的問題: enum 如果不是由 0 開始, 或者 enum 定義的數值當中有空缺, 應該都不適合拿來當作陣列的索引.


所以總合前述二個問題的解決方案: 使用 C++ 時

#include <stdio.h>
#include <assert.h>

#define SIZEOF_ARRAY(arr) (sizeof(arr) / sizeof(arr[0]))

enum Color_t {
      WHITE = 0
    , BLACK
    , GOLD
    , PINK
    , _COLOR_CNT    // 多加這一個
};

void foo()
{
    static const char * const color_name[] = {
          [WHITE] = "white"
        , [BLACK] = "black"
        , [GOLD] = "gold"
        , [PINK] = "pink"
    };
    static_assert(SIZEOF_ARRAY(color_name) == _COLOR_CNT,
        "Size of array 'color_name' does not match with '_COLOR_CNT'.");

    for (int i = 0; i < SIZEOF_ARRAY(color_name); ++i) {
        printf("[%d] = %s\n", i, color_name[i]);
    }
}

使用 C 語言時:

#include <stdio.h>

#define STATIC_ASSERT(COND,MSG) typedef char \
static_assertion_##MSG[(COND)?1:-1]

#define SIZEOF_ARRAY(arr) (sizeof(arr) / sizeof(arr[0]))

enum Color_t {
      WHITE = 0
    , BLACK
    , GOLD
    , PINK
    , _COLOR_CNT    // 多加這一個
};

void foo()
{
    static const char * const color_name[] = {
          [WHITE] = "white"
        , [BLACK] = "black"
        , [GOLD] = "gold"
        , [PINK] = "pink"
    };
    STATIC_ASSERT(SIZEOF_ARRAY(color_name) == _COLOR_CNT, XCOLOR);

    for (int i = 0; i < SIZEOF_ARRAY(color_name); ++i) {
        printf("[%d] = %s\n", i, color_name[i]);
    }
}

不過要小心, WHITE = 0這一行, 千萬不要以非零的數字開頭, 否則字串陣列中會產生 null pointer.

更完善的解決方案 2018/1/24


如果你有用過其他比較後期出現的語言, 尤其是像 perl, PHP, python 之類的腳本式的高階語言 (script language), 那麼你會了解它們都直接在基本資料型態上解決這個問題. 像是:

  • perl 使用的是 hash type.
  • PHP 用的是 array (實際上是一 ordered map) .
  • python 用的是 字典 dictionary.
  • Javascript 則是用 JSON 表示式

如果你的 C 語言專案中有不少這一類的資料型態需要處理, 建議你直接改用 uthash 這個工具程式庫 (library). 整個專案應該會更加的穩定且易於維護. 這一個工具庫除了 uthash 之外, 還有其他好用的高階工具, 像是:

  • utlist
  • utarray
  • utringbuffer
  • utstack
  • utstring


有關 assert() 該不該留在正式釋出的版本:

Static Assert 的相關文件連結

    MagicJackTing 發表在 痞客邦 留言(0) 人氣()