C++11 模板元編程 - 編譯期純函數式計算


我們一直強調把C++模板元編程當做一門圖靈完備的純函數式語言來學習,為了證明它這種能力,之前舉的都是類似計算階乘的簡單例子,這里我們通過模板元編程的編譯期計算能力完成一道相對復雜但是有趣的數學題目。

數一數下圖中一共包含多少個三角形:

答案是24,你數對了嗎?

現在我們想用模板元編程來實現一段程序,讓計算機來幫我們計算結果,該怎么做?

由于模板元編程是一門純函數式語言,用它來解決問題需要函數式編程的思維。函數式的設計思維和數學計算是天生最匹配的:變量不可變,沒有副作用,通過針對問題域構建函數,然后不斷的通過函數組合來描述領域問題。

那么如何做呢?

如上三角形中,我們看到的有點,直線以及由線構成的二維圖形。針對領域問題建模的時候我們需要選取對解決問題有用的領域概念和屬性。那么哪些概念和屬性對領域問題有用呢?這時我們采用自頂向下分析,將目標問題逐級降解,最后得到最底層的構建元素,然后再通過自低向上組合來解決問題。

我們的目標問題是數三角形個數。我們能想到的一種簡單地讓計算機可計算的方法是:遍歷圖中所有三個點的組合,交給一個函數讓它來判斷是否是一個三角形,如果是則累計1,最后就能得到結果。

根據判斷結果累計并計數的函數很容易寫,所以我們關注的焦點轉移到如何判斷三個點是否為一個三角形?判斷三個點是否為三角形需要借助已經存在的這個圖形,這個圖形中給出了點和線的關系,我們需要依賴這種關系來判斷三個點是否為圖上存在的一個三角形。

那么我們要提煉哪些概念和關系來判斷三個點是否為一個三角形呢?對于上圖,任意三個點是否構成一個三角形的判斷方式應該是:三個點兩兩在一條直線上,但是三個點不在同一條直線上。可見我們需要的概念是點和線,并且需要構建一種關系,通過這種關系可以查詢指定的一些點是否在某一條直線上!

為了服務上述目的,我們首先需要點的概念,它的屬性只要一個ID用于區分不同的點即可。然后需要有線的概念,對于線只要知道它上面有哪些點,用于查詢多個點是否在同一條線上。線不需要有ID或者名字等屬性,因為經過分析這些對于我們要解決的問題域沒有用。

經過上述自頂向下的分析,我們得到以下基本的領域元素:

  • 點:需要一個ID或者不重復的名字,用于區分不同的點;
  • 直線:唯一的屬性就是維護哪些點在當前線上;
  • 三角形:三個點,它們滿足兩兩相連但是不同時在一條直線上;

有了三角形的定義,我們判斷三個點是否為三角形的算法的偽代碼如下:

is_triangle(P1, P2, P3)->
    connected(P1, P2) and
    connected(P1, p3) and
    connected(P2, P3) and
    (not in_same_line(P1, P2, P3))

對于上面偽代碼中的算法,再自頂向下分析,可以得到如下子算法:

  • connected(P1, P2):判斷兩個點是否被同一條直線相連。針對我們前面對點和線的屬性定義,這其實就是在判斷由兩個點組成的集合是否為一條線上所有點的集合的子集的算法。

  • in_same_line(P1, P2, P3):判斷三個點是否在同一條線上。和前面一樣,本質是在判斷由三個點組成的集合是否為一條線上所有點的集合的子集。

可見我們用到的算法抽象到數學上都是集合運算。

經過上述自頂向下的分析,我們分解出了解決該問題的領域概念和算法。現在我們來自底向上實現。

利用模板元編程來實現,無非就是在模板元編程中找到合適的語言元素來映射上述分析出來的概念和算法。

首先定義點。我們之前把模板元編程的計算對象已經統一到類型上,那么用來表示點的技術手段就只有C++的類型。為了簡單我們可以用IntType來表示不同的點,如用__int(1)__int(2)表示不同的點。但是為了讓代碼更具類型安全,以及表達力更好,我們仿照IntType為點實現一種專門的類型:

// "tlp/samples/triangle/include/Point.h"

template<int N> struct Point{};

#define __p(N)  Point<N>

現在我們可以用Point<1>Point<2>或者__p(1)__p(2)來表示不同的點了。

接下來我們來定義線,一條線就是在線上所有點的集合。既然點在C++模板元編程中用類型表示,那么點的集合就是一組類型的集合,我們用之前介紹過的TypeList來表示。

#define __line(...)   __type_list(__VA_ARGS__)

同時我們可以用TypeList表示一組點,或者一組線,為了讓表達力更好,我們給出如下別名定義:

#define __points(...)   __type_list(__VA_ARGS__)
#define __lines(...)    __type_list(__VA_ARGS__)

現在我們可以用上面的定義來描述圖形了。如下是我們對前面給出的圖形的描述:

using Figure = __lines( __line(__p(1), __p(2), __p(8))
                      , __line(__p(1), __p(3), __p(7), __p(9))
                      , __line(__p(1), __p(4), __p(6), __p(10))
                      , __line(__p(1), __p(5), __p(11))
                      , __line(__p(2), __p(3), __p(4), __p(5))
                      , __line(__p(8), __p(7), __p(6), __p(5))
                      , __line(__p(8), __p(9), __p(10), __p(11)));

下來我們可以實現判斷三個點是否為三角形的元函數了,我們用下面的測試用例表達出我們對該元函數的期望:

TEST("test triangle")
{
    ASSERT_TRUE(__is_triangle(__triple(__p(1), __p(2), __p(3)), Figure));
    ASSERT_FALSE(__is_triangle(__triple(__p(1), __p(2), __p(8)), Figure));
};

上面的__triple是三個點的集合,它的定義如下:

// "tlp/samples/triangle/include/Triple.h"

__func_forward_3(Triple, __type_list(_1, _2, _3));

#define __triple(...)   Triple<__VA_ARGS__>

可見__triple只不過是限定長度為3的TypeList。

現在針對__is_triangle的測試用例,我們來考慮它的實現。它有兩個參數,第一個為三個點的集合,第二個為整個Figure(本質是一組直線的集合)。它需要判斷三個點在對應的Figure中是否構成一個三角形。

再回到前面給出的這個偽實現:

is_triangle(P1, P2, P3)->
    connected(P1, P2) and
    connected(P1, p3) and
    connected(P2, P3) and
    (not in_same_line(P1, P2, P3))

可以看到實現__is_triangle最主要的是如何定義connectedin_same_line。在前面我們分析過這兩個元函數本質上都是在在判斷:入參中的點的集合是否為Figure中任一直線上所有點的集合的子集。而這個正好可以直接使用TLP中針對TypeList的Belong元函數:

__belong():入參為一個list和一個list的集合Lists,判斷list是否為Lists集合中任一元素的子集,返回BoolType;

TEST("sublist belongs to lists")
{
    using Lists = __type_list( __value_list(1, 2)
                             , __value_list(2, 3));

    ASSERT_TRUE(__belong(__value_list(2, 3), Lists));
    ASSERT_TRUE(__belong(__value_list(3), Lists));
    ASSERT_FALSE(__belong(__value_list(1, 3), Lists));
};

OK,分析至此,我們給出__is_triangle的實現:

// "tlp/samples/triangle/include/IsTriangle.h"

template<typename Triple, typename Figure> struct IsTriangle;

template<typename P1, typename P2, typename P3, typename Figure>
struct IsTriangle<__type_elem(P1, __type_elem(P2, __type_elem(P3, __null()))), Figure>
{
private:
    __func_forward_2(Connected, __belong(__points(_1, _2), Figure));
    __func_forward_3(InSameLine, __belong(__points(_1, _2, _3), Figure));

public:
    using Result = __and( Connected<P1, P2>
                        , __and(Connected<P2, P3>
                               , __and(Connected<P3, P1>, __not(InSameLine<P1, P2, P3>))));
};

#define __is_triangle(...)  typename IsTriangle<__VA_ARGS__>::Result

上面IsTriangle的入參第一個是Triple,第二個是Figure。它通過模板特化萃取出Triple中的三個點P1P2P3,然后調用內部定義的元函數ConnectedInSameLine判斷三個點兩兩相連并且不同時在一條直線上。

有了__is_triangle,我們最后來定義數三角形的算法。我們繼續用測試用例來描述我們對該元函數的設計。

TEST("count triangles")
{
    using Points = __points( __p(1), __p(2), __p(3), __p(4), __p(5), __p(6), __p(7), __p(8), __p(9), __p(10), __p(11));

    using Triples = __comb(Points, __int(3));

    ASSERT_EQ(__count_triangles(Triples, Figure), __int(24));
};

上面測試用例中我們先獲得所有三個點的組合using Triples = __comb(Points, __int(3)),然后把所有的三個點的列表和Figure傳遞給__count_triangles,讓它幫我們計算出其中合法的三角形個數。__comb()已經被TLP庫實現了,我們直接拿來用。

  • __comb():入參為list和一個__int(N),返回由list對于N的所有組合的列表;

對于__count_triangles的實現應該是遍歷每一個Triples的成員,對其調用__is_triangle,對滿足要求的進行累計。對一個列表進行累積的通用算法是fold,TLP庫中已經有了針對TypeList的fold算法實現了:

  • __fold(List, Acc, Func(T1, T2)):折疊算法;將List所有元素按照Func給的算法折疊成一個值返回,Acc是折疊啟動參數;

我們直接使用__fold來實現__count_triangles

// "tlp/samples/triangle/include/CountTriangles.h"

template<typename Triples, typename Lines>
struct CountTriangles
{
private:
    template<typename Sum, typename Triple>
    struct Count
    {
        using Result = __if(__is_triangle(Triple, Lines), __add(Sum, __int(1)), Sum);
    };

public:
    using Result = __fold(Triples, __int(0), Count);
};

#define __count_triangles(...)  typename CountTriangles<__VA_ARGS__>::Result

至此,我們基本上已經實現了這個程序,所有的計算都在C++編譯期完成,我們通過測試用例對設計結果進行了校驗。

// "tlp/samples/triangle/test/TestTriangle.h"

FIXTURE(TestTriangle)
{
    using Figure = __lines( __line(__p(1), __p(2), __p(8))
                          , __line(__p(1), __p(3), __p(7), __p(9))
                          , __line(__p(1), __p(4), __p(6), __p(10))
                          , __line(__p(1), __p(5), __p(11))
                          , __line(__p(2), __p(3), __p(4), __p(5))
                          , __line(__p(8), __p(7), __p(6), __p(5))
                          , __line(__p(8), __p(9), __p(10), __p(11)));

    TEST("test a triple whether a triangle")
    {
        ASSERT_TRUE(__is_triangle(__triple(__p(1), __p(2), __p(3)), Figure));
        ASSERT_FALSE(__is_triangle(__triple(__p(1), __p(2), __p(8)), Figure));
    };

    TEST("count triangles")
    {
        using Points = __points( __p(1), __p(2), __p(3), __p(4), __p(5), __p(6), __p(7), __p(8), __p(9), __p(10), __p(11));

        using Triples = __comb(Points, __int(3));

        ASSERT_EQ(__count_triangles(Triples, Figure), __int(24));
    };
}

使用模板元編程做計算的介紹就到這里。受限于模板元編程IO能力的缺失,以及C++編譯器對模板遞歸層數的限制以及模板的編譯速度,純粹采用元編程來解決問題是比較少見的。模板元編程大多數場合是為“運行期C++”做服務,一起結合來發揮C++語言的強大威力,后面我們將看到這方面的例子。


類型操縱

返回 C++11模板元編程 - 目錄

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容