假設程式需要用到三個演算法,分別是 Foo、Bar、Qwerty,每個演算法可能會有 64-bit 整數、32-bit 整數、SSE2、SSE3 等四種實作方式。

在 32-bit 編譯環境上,通常 32-bit 版本會比 64-bit 版還快,且 SSE 版還會比原生整數更快;反過來說,在 64-bit 編譯環境應當優先使用 64-bit 整數版。基於懶人因素以及現實考量,大部分的演算法在一開始並不會有 SSE 版,只有當現有演算法還有顯著改善空間時,才有足夠的誘因去實作 SSE 版。最後,並非所有平台都支援 SSE,因此必須適時關閉這部份實作。

這就是我遇到的問題。有一個很顯而易見的方案,就是運用條件編譯式插入不同的實作,我之前也是這樣做的。應該很容易想像,當程式碼漸漸擴充,條件編譯式很快就變得難以閱讀。

於是我冒出一個想法,首先用 struct 將演算法實作包裹起來:

struct Data { /*...*/ };

struct AlgoPack64
{
    static void Foo(Data& data);
    static void Bar(Data& data);
    static void Qwerty(Data& data);
};

struct AlgoPackSSE2
{
    static void Foo(Data& data);
};

...

接下來只要把這些 struct 依照優先順序丟到 type list 當中,如此就可以在編譯期篩選適當的實作版本。這裡 AlgoPackSSE2 只實作了 Foo 演算法,假如我們需要 Bar 演算法,AlgoPackSSE2 會被跳過,直到找到可用的 Bar 演算法實作為止。演算法優先順序清單可以運用條件編譯式決定:

#if SOME_COND
typedef boost::mpl::list<AlgoPackSSE3, AlgoPackSSE2,
                         AlgoPack32, AlgoPack64> AlgoList;
#elif OTHER_COND
typedef boost::mpl::list<AlgoPackSSE3, AlgoPackSSE2,
                         AlgoPack64> AlgoList;
#else
typedef boost::mpl::list<AlgoPack32, AlgoPack64> AlgoList;
#endif

...
// 使用端:用 find_if 找到清單中第一個實作 Foo 的 AlgoPackXXX。
using namespace boost::mpl;

typedef find_if<AlgoList, has_Foo<placeholders::_1> >::type iter;
deref<iter>::type::Foo(data);

看起來似乎仍然沒辦法擺脫條件編譯式的糾纏,但總比之前的方法更友善,至少條件編譯式被集中在一起,而且對應的演算法優先順序一目了然。而且只要新的實作被加到 struct 中,就會被 find_if 自動選取,完全不需要再更動政策面的程式碼。現在的問題是,該如何實作 has_Foo 這個 trait?

一個很簡單的想法如下,不僅可以一併檢查型別,還可以確保是 static 成員。只要作一點小修改,同樣的原理也可以用來確保被檢查的成員是 non-static。

#define DEF_STATIC_MEMBER_DETECTOR(memName, memType) \
    template<class T> struct has_##memName { \
        typedef char Yes[3]; \
        typedef char No[5]; \
        \
        template <class Type, Type* Ptr> struct Helper {}; \
        \
        template <class U> \
        static Yes& Test(Helper<memType, &U::memName>* ); \
        \
        template <class U> \
        static No& Test(...); \
        \
        typedef has_##memName type; \
        enum { value = sizeof(Test<T>(0)) == sizeof(Yes) }; \
    };

DEF_STATIC_MEMBER_DETECTOR(Foo, void (Data&));
DEF_STATIC_MEMBER_DETECTOR(Qwerty, void (Data&));

在《More C++ Idiom》當中有一個 Member Detector ,主要是運用了多重繼承的名稱查詢規則。和前面程式碼不同的是,他只能檢查名稱而無法檢查型別。

《More C++ Idiom》的範例比較曲折,可以看出該作者是刻意繞過型態吻合和 static/non-static 問題,這些在實用中進行這些檢查很可能是多此一舉。畢竟實際使用時比較在乎的往往是「型別是否互通」,嚴格檢查「型別是否相同」反而會很難用。

其他想法?

這裡的作法有個更簡單的替代方案:線性繼承。由於靜態函數的繼承都是直接覆蓋名稱,所以我們可以讓優先順序高的實作覆蓋優先順序較低的,優先順序的指派同樣可以借助 mpl::inherit_linearly 來實現。這其實是我最早的想法,不過總覺得有點濫用繼承,所以沒有實際動手去實作。

對於我想解決的問題而言,這裡介紹的方法有點「粒度(granularity)」方面的疑慮。例如所有 SSE2 下的演算法實作都是同進同退,沒辦法單獨調整其中一個演算法的優先順序,不過如此的精細調整實用性存疑。

我個人比較希望把所有演算法都實作成獨立函數,優先順序則可以編成某種 trait,然後運用 mpl 或 enable_if 在多載當中做選擇,但目前想到的幾種方案都有相當的複雜性。

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