使用 C/C++ macro 達成遞迴效果

前言

最近因爲再專案內導入了 json,自然而然地也出現了非常常見的問題:從 json 內提取一推內容。 如果說只拿一個東西出來,這真的不成什麼問題,難處就在於常常一次就要拉四五個變數出來,同時還要應付物件不存在或是型別不對的例外處理,事情就變得有趣起來了。

問題

先來看個簡單地例子吧!

假設我們要從一個 json 物件中抽出長,寬,x 跟 y 的座標,然後回傳一個 optional 給呼叫者,這裏是我們要做的事情:

  • 確定 json 中有 width 屬性,且可被轉型爲 double
  • 把 width 當作 double 拿出來

  • 確定 json 中有 height 屬性,且可被轉型爲 double

  • 把 height 當作 double 拿出來

  • 確定 json 中有 x 屬性,且可被轉型爲 double

  • 把 x 當作 double 拿出來

  • 確定 json 中有 y 屬性,且可被轉型爲 double

  • 把 y 當作 double 拿出來

正式寫成程式碼大概會長這樣:

optional<Rectangle>
someFunction(cosnt Json& json) {
    try {
        const auto x = json.at("x").get<double>();
        const auto y = json.at("y").get<double>();
        const auto width = json.at("width").get<double>();
        const auto height = json.at("height").get<double>();

        // Do something.
        ...

    } catch (...) { // 先不管是找不到元素還是轉型失敗
        return none;
    }
}

Okay,現在我們把東西拿出來了,問題也比較明顯了。在 絕大多數 情況下,我們想提取的東西 會對應上同名的變數,x 對上 json["x"],height 對上 json["height"]。對於提取內容的那段程式碼來說,其實只需要知道 元素名稱型別 而已,其他的都是重複程式碼,所以,我們要來想辦法!

想辦法

辦法 A - template

先說答案,template 並不可行。

沒錯,我們有 variadic argument 跟 variadic type argument 可以用,可是核心的問題是 template 沒辦法幫我們建立變數!!

template 可以把變數從 json 依照給定的型別拉出來,但是不論是 function template 還是 class template 這些變數最終還是要依賴後續的操作才能從裏面拿出東西,並無法達成我們想 偷懶 的目的,所以淘汰。

辦法 B - good-old-macro

C/C++ 中的 macro 是一種 笨的可以的 純文字取代機制,所以要做到

DO_THIS(json, double, x) -> const auto x = json.at("x").get<double>();

並不是什麼難事,加上現在 macro 也支援不定長度的參數輸入,要做到

DO_THIS_ALL(json, double, x, int, y)
->
const auto x = json.at("x").get<double>();
const auto y = json.at("y").get<int>();

也是輕而易舉啦!

啦...

啦..?

事情當然沒有那麼簡單...

Macro 的問題

先假設問題不存在,我們的 macro 應該會長得像下面這樣子:

// 省略終止狀態,反正也寫不出來
#define DO_THIS_ALL(json, type, varName, ...) \
    const auto varName = json.at(varName).get<type>(); \
    DO_THIS_ALL(json, __VA_ARGS__)

實際跑過前處理器就會發現,這招根本行不通。我們預期的結果是前處理器會不斷 展開 DO_THIS_ALL 直到終止狀態出現,但是真正的狀況是,前處理器只會替換 一次 而已。這是 C/C++ 前處理器的規則,當一個 macro 被以 a 規則替代後 ,把規則 a 從 可選清單剔除,然後看有無其他規則可用

雖然不難想像提出這項規則的原因 (想想 #define ABB ABB),但我們還是有辦法利用 macro 來達到類似的遞迴呼叫,來取代成我們想要的字串。

Macro 的遞迴

剛剛我們提過了,macro 的問題是 同一條規則只會替代一次,那我們給你不同規則怎樣?

那就是最終的答案了:

#define DO_THIS_ALL(varNum, json, type, name, ...)                             \
  const auto name = json.at(#name).get<type>();                                \
  _DO_THIS_ALL_##varNum(json, __VA_ARGS__)

#define _DO_THIS_ALL_2(json, type, name, ...)                                  \
  const auto name = json.at(#name).get<type>();
#define _DO_THIS_ALL_3(json, type, name, ...)                                  \
  const auto name = json.at(#name).get<type>();                                \
  _DO_THIS_ALL_2(json, __VA_ARGS__)
...

用起來像這樣

DO_THIS_ALL(3, json, double, x, double, y, int, width)

展開的結果像這樣 (換行是方便閱讀加入的)

const auto x = json.at("x").get<double>();
const auto y = json.at("y").get<double>();
const auto width = json.at("width").get<int>();

蹦!這就是我們想要的結果!

雖然說這方法還是存在某種程度的麻煩 (需要幾層遞迴就需要手寫幾次 _DO_THIS_ALL_XX),但這是目前我所能想出 C/C++ 中最正確的方法了,如果正在看這篇文章的你有任何有趣或是實際的點子的話,也歡迎留言和我分享!

結語

果然純文字取代的 macro system 很麻煩啊,利用 macro 來宣告變數 這件事在 Rust 之類展開 AST 的 macro system 已經是家常便飯了,例如下面是 Rust 中用來初始化陣列內容的 macro:

macro_rules! vec {
    ( $( $x:expr ),* ) => {
        {
            let mut temp_vec = Vec::new();
            $(
                temp_vec.push($x);
            )*
            temp_vec
        }
    };
}

// 用起來像是這樣

let v = vec![1, 2, 3];

// 展開等同於


let v = {
    let mut temp_vec = Vec::new();
    temp_vec.push(1);
    temp_vec.push(2);
    temp_vec.push(3);
    temp_vec
};

不過能找到解法,還是謝天謝地就是了,不停寫着同樣的程式碼不停地複製貼上真的不是很好受。

Comments

comments powered by Disqus