2018年5月25日

[C++] 利用 generic progamming 簡化存取 union 變數的介面

公司的程式 (特別是那種底層 API) 其實很常使用 union 來減少非必要的記憶體使用量,只是如果使用 union 的話就必須要能夠知道該用哪種型別來存取 union 中的變數,因此很常出現這種程式碼:

enum Type {Int, Double};

struct Data {
    Type type;
    union {
        int i;
        double d;
    };
};

int main()
{
    Data data;

    data.type = Int;
    data.i = 3;

    data.type = Double;
    data.d = 3.14;

    return ;
}

簡單來說就是設定好要存取的是哪種型態 (然後用個 enum 來表示),接著 API 會根據設定的型態來對 union 中的變數做存取。以上面的範例來說,會根據 Data 中的 type 是被設定成 Int 還是 Double 來決定 是要存取 data.i 還是 data.d

因為個人覺得這樣滿容易手殘的,特別是公司的 API 其實型別種類有點多 |||Orz,所以想要簡化 API 的使用方式,希望可以直接根據所設定的型別存取對應的變數就好,畢竟這種關聯性其實是固定的,利用 generic programming 應該是不難做到的。



先來個 C++11 能使用的方法:

template <int N> struct UType;

template <>
struct UType<Int>
{
    typedef decltype(Data::i) type;
};

template <>
struct UType<Double>
{
    typedef decltype(Data::d) type;
};

template <int N>
typename UType<N>::type get_data(Data& data);

template <>
UType<Int>::type get_data<Int>(Data& data)
{
    data.type = Int;
    return data.i;
}

template <>
UType<Double>::type get_data<Double>(Data& data)
{
    data.type = Double;
    return data.d;
}

int main()
{
    Data data;

    data.i = 3;
    std::cout << "value: " << get_data<Int>(data) << '\n'
              << "type: "  << data.type << '\n';

    data.d = 3.14;
    std::cout << "value: " << get_data<Double>(data) << '\n'
              << "type: "  << data.type << '\n';

    return 0;
}

概念其實很簡單:先定義一個 struct UType,裡面只有一個 typedef 後的型別 type,利用全特化 (full specialization) 就可以讓 type 跟據 enum 的值來改變應該是哪種型別。

接著再自訂另一個 function 當介面 (上面的例子中就是 get_data),回傳型別就只要利用剛剛定義好的 Utype<N>::type 即可,這樣就可以根據 enum 的值讓 compiler 決定應該是哪種型別;回傳值呢? 一樣利用全特化來決定應該回傳 union 中的哪個變數,如果給的是 Int 那就回傳 data.i;如果是 Double 就回傳 data.d。設定好這個中介介面後就能像 main 中那樣的使用方式只指定要使用的是哪種型別就可以讓 compiler 挑到適合的介面來得到正確的值。

上面的例子雖然說是用 C++11,不過其實只要把 decltype 改掉,直接寫明是用哪種型別的話,在 C++98 應該也是可以正確使用的。


好的,其實這問題在 C++14 會變得更簡單,因為 C++14 有 auto return type deduction,所以其實可以簡化成下面這樣:

template <int N> auto get_data(Data& data);

template <>
auto get_data<Int>(Data& data)
{
    data.type = Int;
    return data.i;
}

template <>
auto get_data<Double>(Data& data)
{
    data.type = Double;
    return data.d;
}

int main()
{
    Data data;

    data.i = 3;
    std::cout << "value: " << get_value<Int>(data) << '\n'
              << "type: " << data.type << '\n';

    data.d = 3.14;
    std::cout << "value: " << get_value<Double>(data) << '\n'
              << "type: " << data.type << '\n';

    return 0;
}

對,連 UType 都免了...C++11 開始雖有不少噁心的東西出現,不過方便的東西其實也不少,至少程式碼真的有機會簡潔許多。

沒有留言:

張貼留言