2019年3月26日

[C] Variadic Macro

有時候會有個需求是像這樣:
  • 希望在執行任何 function / expression 前可以先做檢查,檢查過了才真的執行
  • 能把上述的檢查包成容易使用的形式,讓後續使用的人不會忘記
這種需求其實很常見,以 C 來說,確保一個 pointer 不是 null 後才真的拿來用是很基本的事情。所以如果可以包成像下面的形式想必會好用很多也比較不會忘記:

#define CHECK_AND_USE(x, ...) if (x) { x(...); }

typedef void (*f1ptr)(int);
typedef void (*f2ptr)(double, const char*);
f1ptr p1 = NULL;
f2ptr p2 = NULL;
CHECK_AND_USE(p1, 2) // equivalent to if (p1) { p1(2); }
CHECK_AND_USE(p2, 1.0, "test") // equivalent to if (p2) { p2(1.0, "test"); }

這樣基本上只要記得都透過 CHECK_AND_USE 就能避免用 pointer 前忘記先做 null check。重點來了:CHECK_AND_USE 中的 ... 該怎麼辦呢?顯然的,我們需要不定數量與型態的參數。而能達到這件事情的有幾個方式,這邊要介紹的是 C99 開始支援的 Variadic Macro

類似 variadic macro 的東西我想一般最容易想到的是 variable-length argument (va_list),因為 C 的 printf 系列就是用這玩意兒做到的。不過如果細看規格的話就會發現這跟我們上面想要的還是不太一樣:因為其實在這種情境下其實我們不太介意傳進來的參數型態跟數量 (也就是上面例子 ... 的部分),只希望把它直接全部丟給真正要用的參數 (也就是上面例子的 x)。

在 C++11 後引入的 variadic template 其實已經很接近上面要做的事情了,但是還是會有個小地方不同,舉例來說:

template <typename Func, typename ... Args>
void CHECK_AND_USE(Func f, Args&& ... args)
{
    if (f) { f(std::forward<Args>(args)...); }
}

CHECK_AND_USE(p1, 2);
CHECK_AND_USE(p2, 1.0, "test");
CHECK_AND_USE(p3, p1(2)); // p1(2) will be executed before passing to CHECK_AND_USE

前兩個跟上面的例子一樣就不多說,重點在第三個:因為 CHECK_AND_USE 在 variadic template 的情境下依然是個 function,而 C/C++ 會在進入 function 前先把所有的 argument 都執行一遍確保得到一個值後才真的進入 function 本體 (詳情請參閱 sequence point,C++11 之後改叫 sequenced before)。因此如果今天希望在檢查不通過的情況下所有的參數都不會被執行到,以上面的例子來說就是 p3 是 NULL 時 p1(2) 也不會被執行,那麼 variadic template 在這情況下也不適用。

好消息是雖然這問題有辦法解決,請參閱 lazy evaluation for function argument 這篇,壞消息則是這必須仰賴使用者自己逐一處理,沒辦法做到自動替換,因此最合用的東西其實就是 variadic template

用法其實滿單純的:

#define eprintf(format, ) fprintf (stderr, format, __VA_ARGS__)

前處理器會把放在 __VA_ARGS__  (注意 VA_ARGS 的前面跟後面都是兩個底線) 用前面 ... 的所有東西全部替換掉,就這樣。

這邊要再次強調 variadic template 跟 variadic macro 有本質上的不同,因此雖然看似相似,但同樣的寫法可能會有完全不同的結果,因此沒有哪個比較好,須謹慎視情況使用。

沒有留言:

張貼留言