[Note] 神的語言 Metaprogramming: one_of

發想

最近在寫程式的時候遇到一個情景,讓我非常困擾。
下面這個情況我不需要多加解釋,應該很多人也都有遇過類似這種困擾。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void test_status(STATUS_t unknown_status)
{
if(unknown_status == STATUS_1 || unknown_status == STATUS_3 || unknown_status == STATUS_5 || unknown_status == STATUS_7)
{
// do some stuff
}

if(unknwon_status == STATUS_2 || unknown_status == STATUS_3 || unknown_status == STATUS_4)
{
// do some stuff
}

if(unknwon_status == STATUS_5 || unknwon_status == STATUS_6 || unknwon_status == STATUS_9 || unknwon_status == STATUS_11 || unknwon_status == STATUS_27 || unknwon_status == STATUS_38)
{
//do some stuff
}
}

當你遇到狀況 1、3、5、7 時要處理一些事情,遇到狀況 2、3、4 時要處理一些事情,最後遇到狀況 5、6、9、11、27、38 的時候又要再處理一些事情….,可能有些人遇到的,後面還要再拉更多的 if 跟重複寫一堆相同且沒有意義的變數名 (unknown_status) 跟operator (||)。不但程式變得很長,不容易閱讀,你也很容易在寫這一長串的時候不小心出錯,例如 operator == 寫成 operator = ,結果還 de 不出 bug,諸如此類的小陷阱。

當然你可以抱怨說,到底是誰這麼沒水準,定義這種沒有規則的 STATUS。但是有時候可能你因為一些被限制的因素而只能使用這種不符合你預期的規則的 lib ,你也無從選擇只好接受。

於是就讓我萌生了一些想法:我有沒有辦法用一個很簡單的表達式來省略掉這些高度重複的變數名、還有 operator。當然其實我早就知道這是可行的,而且方法非常多,隨便想都可以想的到 3~5種偷懶的方法,像是開個 vector 把狀況們都捆成一包,再用 for 回圈去檢查、或是用 std::any_of 搭配 lambda function 的方式解決,又甚至自己重新 mapping 一次 STATUS,變成可以使用 Binary OR 的方式檢查。

問題就在於,我要如何在解決問題的同時又能夠解決的漂亮,這是一個很大的問題。我當然我也可以選擇不動腦就寫一堆垃圾 Code 來解決這種不起眼的小問題,但是這就不是我的 Style 啦。於是我決定要動手設計了一個新的 operator (我稱他是 operator 啦,雖然他只是一堆 function 跟 struct 的疊加),這個 operator 的特點就是看起來要極其順眼,非常容易使用,最重要的是在 compile 之後的效能要能夠跟原本的暴力破解垃圾 Code 不相上下。

於是我第一個想到的 operator 就是 one_of,這是我的目標:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void test_status(STATUS unknown_status)
{
if(unknown_status == one_of(STATUS_1, STATUS_3,
STATUS_5, STATUS_7))
{
//do some stuff
}

if(unknwon_status == one_of(STATUS_2, STATUS_3, STATUS_4))
{
// do some stuff
}

if(unknwon_status == one_of(STATUS_5, STATUS_6,
STATUS_9, STATUS_11,
STATUS_27, STATUS_38))
{
//do some stuff
}
}

跟原本的寫法比起來是不是變得更順眼、更易讀、更易寫(不容易出錯)?這個 operator 非常口語化的詮釋了我想做的事情:

  • if: 當
  • unknown_status: 某變數
  • ==: 等於
  • one_of: 下列其中一個
  • STATUS_1, STATUS_3, STATUS_5, STATUS_7
  • 我就 do some stuff

目標

1
2
3
4
5
if(unknown_status == one_of(STATUS_1, STATUS_3,
STATUS_5, STATUS_7))
{
//do some stuff
}

我想要定義一個新的 operator one_of 來解決掉一堆難看的垃圾 Code 問題。而且他擁有以下特點:

  • 易寫、易讀
  • 可以接受不定個數的參數 (可以使用 parameter pack 實現)
  • 可以接受任何型態 (可以使用 template 實現)
  • 編譯後效能可以跟原本的垃圾 Code 一樣好 (metaprogramming 實現)

你看看,從上列開出的特點來看,就是只能用 metaprogramming 實現了。

實現

好了,現在有了目標,問題就在於要如何實現這個 one_of?

首先,可以看的出來,我們要把 unknown_status 跟 one_of(…) 做比較,能做到的方法其實就幾個:第一個是 one_of 可能是一個 struct/class,他的 Constructor 能夠接納無限個 parameters,然後我拿某個變數 unknown_status 跟這個 struct/class 做 == 比較 (operator overloading)。第二個是 one_of 可能是一個 function,他能夠接納無限個 parameters,呼叫後會回傳一個包好的 struct/class (我傳入的 parameters 都在裏面),然後再用 unknown_status 去跟這個 struct/class 做 == 比較 (operator overloading)。

我們使用第一個方法來實作,流程是這樣,one_of 的 constructor 可以接受 parameter pack,之後我們將 parameter pack 存到 std::tuple 裏面放著,等到呼叫 == 時再從 std::tuple unpack 一個一個做判斷:

所以第一步先建立好 one_of 的 constructor (這邊使用 struct 是因為 struct 的 member 預設是 public,我們不需要再多一步用 public: 來指定)

1
2
3
4
5
struct one_of
{
template<typename... Ts>
one_of(Ts&&...args);
}

這邊的語法 typename...Ts 就是 parameter pack,意思就是我會傳入不定個數的參數。不多解釋,不懂的自己去 google。接下來我們要把傳進來的參數 args 包成一份 std::tuple,而因為 std::tuple 也需要不定個數的欄位,我們勢必必須把 struct one_of 也宣告成 template struct:

1
2
3
4
5
6
7
8
9
10
11
#include <tuple>
#include <utility>

template<typename... ArgTypes>
struct one_of
{
std::tuple<ArgTypes...> args; //tuple

template<typename... Ts>
one_of(Ts&&...args) : args(std::forward<Ts>(args)...){}; // constructor
};

這邊我們使用 std::tuple 因此我們必須 #include <tuple>,還有我們使用 std::forward 來 unpack parameter pack 變成一列 arguments 傳進 tuple 的 constructor 裏面結束這回合,因此我們還必須 #include <utility>

但是接下來我意識到一件事情,如果我們使用這種方法來設計我們的 one_of 的話,由於現在 one_of struct 已經變成 template struct 了,到時候呼叫的方法就會變成:

1
if(unknown_status == one_of<STATUS_t, STATUS_t, STATUS_t, STATUS_t>(STATUS_1, STATUS_3, STATUS_5, STATUS_7))

喔不! 我們必須指定傳入參數的型態給 template!這可不是我們當初所預期的 one_of 啊!
但是不用擔心,他還有救,讓我們來給他包上一層 helper function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <tuple>
#include <utility>

template<typename... ArgTypes>
struct _type_one_of
{
std::tuple<ArgTypes...> args; //tuple

template<typename... Ts>
_type_one_of(Ts&&...args) : args(std::forward<Ts>(args)...){}; // constructor
};

template<typename... ArgTypes>
constexpr auto one_of(ArgTypes&&... args) -> _type_one_of<ArgTypes...>
{
return _type_one_of<ArgTypes...>(std::forward<ArgTypes>(args)...);
}

我做了什麼事情呢?
首先,我把 struct one_of 改名了,改成 struct _type_one_of 接著我新增了一個 helper function 就是我們最愛的互動介面 one_of。他的功能就是當我們傳入參數到 one_of 時他會幫我們建立一個包好的 struct _type_one_of 這樣我們就不用自己手動包了!

helper function 的 return type 是 _type_one_of<ArgTypes...> 而使用 -> 的寫法稱做 trailing return type,他只是可以延後到 function declaration 後面才指定 return type,在這個 case,這樣寫跟把 return type 寫在前面其實沒有差別,單純只是我覺得因為 return type 長的比較醜,放在後面這樣比較好看。而回傳的東西一樣就是把東西 unpack。

除此之外,在寫 metaprogramming 的時候要記得,你希望 compiler 自動幫你拆開來的 function 都要加上 constexpr specifier,這樣 compiler 才會盡可能幫你拆開。而 std::forward 這個 function,很幸運的在 c++ 14 的時候已經改成 constexpr,因此這個 constructor 我很有信心 compiler 絕對會幫我們拆開來。

這樣就完成了我們的 one_of,接下來是要寫 == 的部份。我們可以用 operator overloading 來自定義一個 operator==,把任意 type 跟 struct _type_one_of 做比較:

1
2
3
4
5
template<typename T, typename... ArgTypes>
constexpr bool operator==(const T& lhs, const _type_one_of<ArgTypes...> &rhs)
{
return rhs.__match_op(lhs, rhs.args);
}

這邊我們定義好了 operator== 的部份,而接下來因為我們需要把預先存起來的 std::tuple 拿出來用,因此我希望我們可以把 unpack tuple 的部份使用 struct 內部的 function 來實現。當然你也可以不要像我一樣,你也可以直接把 function 定義在外面。

接下來實作 unpack tuple 的部份,回到 struct _type_one_of 裏面:

1
2
3
4
5
6
7
8
9
10
11
template<typename... ArgTypes>
struct _type_one_of{
std::tuple<ArgTypes...> args;

template<typename... Ts>
_type_one_of(Ts&&... args): args(std::forward<Ts>(args)...) {}

template<typename T, typename... Ts, typename Inds = std::make_index_sequence<sizeof...(Ts)>>
constexpr bool __match_op(const T& lhs, const std::tuple<Ts...> &tup) const
{ return __match_op_impl(lhs, tup, Inds{}); }
};

首先我們在 operator== 裏面呼叫了 __match_op 這個 function,因此我們定義一下 __match_op 。第一個參數是型別為 Tlhs (left-hand-side),第二個是我們的 tuple tup。然後我們產生一個 std::integer_sequence,並且呼叫 __match_op_impl

std::make_index_sequence 定義在 <utility>,是 c++ 14 才有的 type。他的功能是可以產生一個 template parameters 為一個數列的 class。而 sizeof...(Ts) 是 c++ 11 的語法,他其實是叫作 sizeof... operator,用途是計算 parameter pack 裏面元素的數量。因此當我呼叫 std::make_index_sequence<sizeof...(Ts)> 時,假設 Ts 裏面有 5 個元素,他會產生一個長的像這樣的 type:

1
std::index_sequence<0, 1, 2, 3, 4>

然後我們定義 template 的最後一個 parameter type 的預設 type 是這個東西,這樣我們就得到
1
typename Inds = std::index_sequence<0, 1, 2, 3, 4>

這個 Inds 是一個 class,因此我們在呼叫 __match_op_impl 並把他當參數傳入時,使用 Inds{} 等於是創建一個 object 的 instance。

之所以要用這種二段式呼叫的原因主要是因為 std::tuple 限制的關係,如果要取得 tuple 裏面的元素,我們必須使用 std::get 這個 function,而 std::get 這個 function 會需要指定元素的 Index。例如,如果要取出 tuple 的第N個元素,則我們必須這樣寫:std::get<N>(tup)。因此,我們利用兩段式呼叫,第一次呼叫先用 std::make_index_sequence 取得元素 Index 的 sequence 後再進行第二次呼叫,unpack tuple。

接下來是定義__match_op_impl 的部份:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<typename... ArgTypes>
struct _type_one_of{
std::tuple<ArgTypes...> args;

template<typename... Ts>
_type_one_of(Ts&&... args): args(std::forward<Ts>(args)...) {}

template<typename T, typename... Ts, std::size_t... I>
constexpr bool __match_op_impl(const T& lhs, const std::tuple<Ts...> &tup, std::index_sequence<I...>) const
{ return __match_one_of_op(lhs, std::get<I>(tup)...); }

template<typename T, typename... Ts, typename Inds = std::make_index_sequence<sizeof...(Ts)>>
constexpr bool __match_op(const T& lhs, const std::tuple<Ts...> &tup) const
{ return __match_op_impl(lhs, tup, Inds{}); }
};

這邊我們定義了 __match_op_impl 函數,一樣,第一個參數是 lhs,第二個是 tuple tup,第三個是 Index sequence,他的 type 是 std::index_sequence<I...> 由於實體變數我們並不是很 care (甚至這個 class 裏面根本沒包多少東西,重點是他的 template parameter pack),所以我們第三個參數只寫 type 而沒有寫 variable 的名稱。我們在 template 裏面定義 Index sequence 的 template parameter pack std::size_t... I。這樣我們就可以用 std::get&ltI>(tup)... 來讓 Compiler 自動幫我們 unpack tuple。

接著我們呼叫 __match_one_of_op,我們第一個參數傳入 lhs,後面的參數則是用 ... 來 unpack。

1
2
3
4
5
6
7
8
template<typename T, typename... ArgTypes>
constexpr bool __match_one_of_op(const T& lhs, ArgTypes&&... args)
{
if constexpr (sizeof...(args) == 0)
return false;
else
return any( (lhs == std::forward<ArgTypes>(args)) ...);
}

這邊我們定義 __match_one_of_op 的內容,首先這先使用了一個 if constexpr else 的表達式,從 c++ 17 開始可以指定 if else 是 constexpr,這樣 compiler 就會幫我們拆開來。而如果你想要使用 constexprif else if else 的話,你可以這樣寫:

1
2
3
if constexpr (/*...*/)
else if constexpr (/*...*/)
else

實際上 else if 就是 else{ if(/*...*/){} }

如果 sizeof...(args) == 0 的話,也就是如果 parameter pack 裏面一個東西都沒有,我們直接 return false。否則我們要把 args... 拆開來一個一個跟 lhs 做比較。

到這邊,如果 compiler 把 any 裏面的東西拆開來,他會得到類似這樣的東西 (這只是pseudocode):

1
any( lhs==args[0], lhs==args[1], lhs==args[2], ...) 

而由於我們當初定義 one_of 是:只要其中一項等於 lhs 就會回傳 true,因此我們可以寫一個 any 這個 function 來負責統整所有比較的結果,只要有其中一個 expression 是 true,則 any 會回傳 true,否則回傳 false

因此我們定義 any 這個 function

1
2
template<typename... ArgTypes>
constexpr bool any(ArgTypes&&... args) { return (... || args); }

這邊我們使用 c++ 17 的語法 (... || args) 這個語法叫作 fold expression,他的用途就是他會把 parameter packs 拆開後中間全部用同樣的 operator 連接起來,因此會得到類似這樣的效果 (這只是pseudocode):

1
(... args[2] || args[1] || args[0])

這邊需要注意的是我寫成 left fold 的型式,但其實也可以使用普通的 right fold 型式
(args || ...)。差別只在於展開的方向不同。

left fold:

1
(... args[2] || args[1] || args[0])

right fold:

1
(args[0] || args[1] || args[2] ...)

詳細請看:fold expression(since C++17) - cppreference.com

到這邊我們就真正完成了我們的 one_of operator,下面是完整的 code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <tuple>
#include <utility>

template<typename... ArgTypes>
constexpr bool any(ArgTypes&&... args) { return (... || args); }

template<typename T, typename... ArgTypes>
constexpr bool __match_one_of_op(const T& lhs, ArgTypes&&... args)
{
if constexpr (sizeof...(args) == 0)
return false;
else
return any( (lhs == std::forward<ArgTypes>(args)) ...);
}

template<typename... ArgTypes>
struct _type_one_of{
std::tuple<ArgTypes...> args;

template<typename... Ts>
_type_one_of(Ts&&... args): args(std::forward<Ts>(args)...) {}

template<typename T, typename... Ts, std::size_t... I>
constexpr bool __match_op_impl(const T& lhs, const std::tuple<Ts...> &tup, std::index_sequence<I...>) const
{ return __match_one_of_op(lhs, std::get<I>(tup)...); }

template<typename T, typename... Ts, typename Inds = std::make_index_sequence<sizeof...(Ts)>>
constexpr bool __match_op(const T& lhs, const std::tuple<Ts...> &tup) const
{ return __match_op_impl(lhs, tup, Inds{}); }
};

template<typename... ArgTypes>
constexpr auto one_of(ArgTypes&&... args) -> _type_one_of<ArgTypes...>
{
return _type_one_of<ArgTypes...>(std::forward<ArgTypes>(args)...);
}

template<typename T, typename... ArgTypes>
constexpr bool operator==(const T& lhs, const _type_one_of<ArgTypes...> &rhs)
{
return rhs.__match_op(lhs, rhs.args);
}

接著你可以試試看使用這個程式

1
2
3
4
5
6
7
8
9
10
int main(void)
{
int g;
std::cin >> g;
if(g == one_of(10, 20, 30, 40, 50))
{
std::cout << "It's a multiple of 10 !" << std::endl;
}
return 0;
}

  • one_of 裏面可以塞不定個數的參數
  • one_of 可以塞任何型態的變數

你可以在這邊 比較一下看看編譯後的結果是不是跟原本的垃圾 Code 一模一樣。

除此之外,因為我們使用 template parameter pack 的關係,one_of 可以傳入每個型態都不一樣的參數:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int main(void)
{
int g;
std::cin >> g;
const int i = 35;
const float f = 12.6;
const double d = -4.9;
const std::string str = "Hello";
const std::vector<double> vd{-1.4, 6.8};
if(g == one_of(i, f, d, str, vd)) // int, float, double, string, vector
{
std::cout << "g is in the set !" << std::endl;
}
return 0;
}

但是這樣你必須自己 overload 不同型態間的 operator==,不然 compile 的時候會出錯。

進階

做比較測試

雖然 one_of 已經可以跟任意型態做比較了,但是實際上這麼做是非常危險的。如上所言,有時候使用者並不會記得要實作出對應任意型態的 operator==,甚至,為每一對型態實作一組 operator== 是非常費時的時間,因此我們有沒有辦法寫個功能讓 compiler 自動判定兩個型態能不能做 == 比較,如果可以的話就做比較,不行的話就回傳 false

這裡我們就必須使用一個 metaprogramming 的特殊技巧叫作 SFINAE,他的核心理念就是實作一個 General 的 template,再實作一個專做測試用的 Specialized template,如果我們想要的功能能夠吻合到 Specialized template 表示測試合格(e.g. 測試某 Type 擁有某個 member、測試某 Type 有支援某 Operator等等),如果不合格,Compiler 也會自動把他吻合到 General 的 template 上面而不會跳出 Compiler error。這個的運作原理不難理解,我在這邊就不多做解釋,想知道的自行 google

因此我們繼續更改 Code,我希望在 __match_one_of_op 裏面呼叫 any 前先加入比較測試,讓 Compiler 幫我們檢查兩個型態能不能做比較 (重點:要 Compiler 幫我們檢查!)

1
2
3
4
5
6
7
8
template<typename T, typename... ArgTypes>
constexpr bool __match_one_of_op(const T& lhs, ArgTypes&&... args)
{
if constexpr (sizeof...(args) == 0)
return false;
else
return any( (__match_comparable_one_of_op(lhs, args)) ...);
}

這邊我們直接呼叫__match_comparable_one_of_op 並用 ... 來幫我們逐一配對檢查看能不能做比較。

這邊的實作方式非常多,我們也可以使用 tag dispatching 、SFINAE、或是std::enable_if 的方式實作,也可以直接用 if constexpr else 的方式實作,而這次我們就先從簡用 if constexpr else 的方式實作。實際上我覺得用 SFINAE 實作我覺得比較優美,因為在 metaprogramming 中出現 if else 這種東西在瀏覽 Code 的時候感覺就是特別礙眼。

1
2
3
4
5
6
7
8
template<typename LT, typename RT>
constexpr bool __match_comparable_one_of_op(const LT& lhs, const RT& rhs)
{
if constexpr (_whether_support_op<LT, RT>::value)
return lhs==rhs;
else
return false;
}

這邊首先使用 struct type 的 SFINAE _whether_support_op 來判斷 LTRT 這兩個型態能不能做比較。
註:SFINAE 也有 function type 的,有機會再介紹。

如果 LTRT 可以做比較,則回傳 lhs == rhs 比較結果,否則回傳 false。這邊注意因為他是 constexpr specified 的 if else 因此如果 if 的條件不成立則 Compiler 不會編譯 if 裏面的內容。有需要的話,我們甚至可以印出一些資訊來看看程式的運作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<typename LT, typename RT>
constexpr bool __match_comparable_one_of_op(const LT& lhs, const RT& rhs)
{
if constexpr (_whether_support_op<LT, RT>::value)
{
std::cout << "[Comparable] ";
return lhs==rhs;
}
else
{
std::cout << "[Not Comparable] ";
return false;
}

}

接下來就是實作 struct 型的 SFINAE _whether_support_op

1
2
3
4
5
6
7
8
template<typename, typename, typename = std::void_t<>>
struct _whether_support_op : std::false_type
{};

template<typename LT, typename RT>
struct _whether_support_op<LT, RT, std::void_t<
decltype(std::declval<LT>()==std::declval<RT>) >> : std::true_type
{};

我也是最近才注意到 c++ 17 中出現了一個新的 Type 叫作 std::void_t 而且根據 cppreference.com 的資訊,這個 Type 就是專門拿來玩 SFINAE 的!由於 std::false_typestd::true_typestd::void_t 都是出自 <type_traits> ,因此必須加上 #include <type_traits>。而 std::void_t 其實有個很有趣的事情就是不管我們塞入什麼型態,最後的 Type 他都會是 void。他的定義類似這樣:

1
2
template<typename...>
using void_t = void;

所以不管我們塞什麼型態給他,他都是 void。

首先先從 General 的 struct _whether_support_op 開始 (他其實有個名字叫作 primary template),這邊定義他的 template 參數是 <typename, typename, typename = std::void_t<> >。之所以都不寫名字是因為我們根本不 care 那個變數型態(簡稱變態)叫作什麼名字,反正他就是會有三個變態進來,然後第三的變態預設為 std::void_t<>就是為了玩 SFINAE 用的。

如果 Compiler 在配對 _whether_support_op 的時候配對到這個 General 版的 ,就表示我們想要的功能無法使用,因此我們讓這種 General 版的 struct 繼承 std::false_type。繼承這個 std::false_type 的時候,_whether_support_op 會繼承到一個 static member 叫作 value,而且 value 值會是 false。因此當我們呼叫 _whether_support_op<LT, RT> 後去取得他的 value 值,會得到 false

1
static_assert(false == _whether_support_op<LT, RT>::value);

接下來定義一個 Specialized 的 struct _whether_support_op (specialized template),這邊 template 只需要定義兩個變態 <typename LT, typename RT> 就可以了,因為第三個變態是我們要玩 SFINAE 用的。接下來就是客製化,這個行為稱做 partial specialization,我們只真對部份的變態做 specialization:

1
struct _whether_support_op<LT, RT, std::void_t</*..specialize..*/> >

可以看出,LTRT 前兩個變態沒有特別 specialize, 但是第三個變態,我們指定他是 std::void_t,並且在 std::void_t 的 template 變態塞入 decltype(std::declval<LT>()==std::declval<RT>)。如果 Compiler 成功配對這個 struct 的話,他會使用這個 specialized 的 struct,而這個 specialized 的 struct 有繼承 std::true_type, 同 std::false_type,如果去取他的 value 值會得到 true

1
static_assert(true == _whether_support_op<LT, RT>::value);

至於,解釋 decltype(std::declval<LT>()==std::declval<RT>) 這一串東西是什鬼,要先從 decltype 開始解釋。decltypestd::declval 都不是新東西了,他們在 c++ 11就存在了。decltype 的用途是可以得到 decltype(expression) 裏面 expression 的回傳型態。例如:

1
2
3
int x = 3;
int y = 5;
decltype(x+y) z = x+y;

我們可以知道 xy 都是 int 型態,所以 x+y 也會回傳 int 型態,利用 decltype 這個 operator,可以得到 x+y 的回傳型態 int 之後再宣告一個新變數 zz 的型態就會是 int

std::declval 定義在 <utility> 裏面,他可以將一個指定的 Type 轉換成該 Type 的 Reference type,但是他並不會呼叫該 Type 的 Constructor,藉此我們可以呼叫他的 member function。所以 std::declval<LT>()std::declval<RT>() 就會分別產生一個 LTRT 的 reference type LT&&RT&& ,我們可以呼叫他們的 member function,或是 operator。但是要注意的是,並不是只要用 std::declval 就可以無限上綱,首先他不會產生一個實體的 instance/object,再來就是他只能用在類似 sizeofdecltype 這類只需要 function definition 的 specifier 上,以及他還有一些規則,如果傳入的變態是 non cv-qualified (非 const 或 volatile) 或是 non ref-qualified (非 lvalue type &) 則會回傳 rvalue type &&,而如果傳入的參數是cv-qualified 或是 ref-qualified 則會回傳同樣的變態。詳細的自己 google

總之,當我們呼叫 std::declval<LT>()std::declval<RT>() 時 Compiler 會產生這兩的變態的 reference type 接著使用 std::declval<LT>() == std::declval<RT>() 嘗試呼叫這兩個變態的 operator==,然後取得回傳的變態 decltype(std::declval<LT>() == std::declval<RT>()),然後將這個變態放入 std::void_t<...>,最後放入 struct _whether_support_op 的第三個 argument。

當我們從外部宣告一個實體 struct 時 (e.g. _whether_support_op<int, std::string> ),Compiler 會發生一系列事情,這邊就會關係到 Compiler 在呼叫 template function 或 template class 的決策流程:

  • 第一個階段會先進行 name lookup,找出對應名稱的 function / class
  • 第二個階段會進行 template argument deduction,推導出所有 candidate function / class
  • 第三個階段會進行篩選,選出最吻合的 function / class

詳細請看:Template argument deduction - cppreference.com

以我們的例子來說,第一個階段的 name lookup,Compiler 可以得到我們有兩個 _whether_support_op 的 template struct。

第二個階段 argument deduction 就會產生變化了,首先他會將指定的型態 intstd::string 帶入第一個 _whether_support_op (注意,到這邊為止,Compiler 都還不知道誰是 primary 誰是 specialized)。由於我們只有指定兩個型態,第三個我們使用預設的 typename = std::void_t<>,Compiler 會產生第一個可行的候選名單 _whether_support_op<int, std::string, void>,但是注意,這個候選名單第三個變態是 default,而非在宣告時指定的。

接下來 deduce 第二個 _whether_support_op 會產生兩種狀況。
狀況一:如果我們有宣告 LTRT 的 operator==,則 std::declval<LT>() == std::declval<RT>() 判斷式會成立,decltype可以得到正確的回傳變態 (通常是 bool),接著 std::void_t<bool> 也能夠正常成立,最後得到完整的 _whether_support_op<int, std::string, void>,這邊的第三個變態就是宣告時指定的,他是從 std::void_t<> 特化成 void 的,因此 Compiler 會把這個 struct 判斷成是一種 specialization。而 specialized template 的優先權會大於 primary template,因此 Compiler 最後會選擇這個 template。這時候我們取出 value 值會得到 true

狀況二:如果我們沒有宣告 LTRT 的 operator==,則 std::declval<LT>() == std::declval<RT>() 判斷式無法成立,decltype 得不到正確的回傳變態,std::void_t<...> 也無法成立,最後 Compiler 沒有辦法得到完整的 specialization,因此這個 struct 就會被 Compiler 從 candidate list 裏面剔除。Compiler 最後選擇使用 primary template。這時候我們取出 value 值會得到 false

最後的程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include <tuple>
#include <utility>
#include <type_traits>

template<typename... ArgTypes>
constexpr bool any(ArgTypes&&... args) { return (... || args); }

template<typename, typename, typename = std::void_t<>>
struct _whether_support_op : std::false_type
{};

template<typename LT, typename RT>
struct _whether_support_op<LT, RT, std::void_t<
decltype(std::declval<LT>()==std::declval<RT>) >> : std::true_type
{};

template<typename LT, typename RT>
constexpr bool __match_comparable_one_of_op(const LT& lhs, const RT& rhs)
{
if constexpr (_whether_support_op<LT, RT>::value)
return lhs==rhs;
else
return false;
}

template<typename T, typename... ArgTypes>
constexpr bool __match_one_of_op(const T& lhs, ArgTypes&&... args)
{
if constexpr (sizeof...(args) == 0)
return false;
else
return any( (__match_comparable_one_of_op(lhs, args)) ...);
}

template<typename... ArgTypes>
struct _type_one_of{
std::tuple<ArgTypes...> args;

template<typename... Ts>
_type_one_of(Ts&&... args): args(std::forward<Ts>(args)...) {}

template<typename T, typename... Ts, std::size_t... I>
constexpr bool __match_op_impl(const T& lhs, const std::tuple<Ts...> &tup, std::index_sequence<I...>) const
{ return __match_one_of_op(lhs, std::get<I>(tup)...); }

template<typename T, typename... Ts, typename Inds = std::make_index_sequence<sizeof...(Ts)>>
constexpr bool __match_op(const T& lhs, const std::tuple<Ts...> &tup) const
{ return __match_op_impl(lhs, tup, Inds{}); }
};

template<typename... ArgTypes>
constexpr auto one_of(ArgTypes&&... args) -> _type_one_of<ArgTypes...>
{
return _type_one_of<ArgTypes...>(std::forward<ArgTypes>(args)...);
}

template<typename T, typename... ArgTypes>
constexpr bool operator==(const T& lhs, const _type_one_of<ArgTypes...> &rhs)
{
return rhs.__match_op(lhs, rhs.args);
}

如此一來我們就可以用來做更狂的比較,還不會跳 Error 出來:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
template<typename T>
bool is_in_the_set(const T& X)
{
if(X == one_of(10,
23.5465,
"Hello",
std::string("foo"),
std::vector<double>{12.5, 64.5},
'c') )
{
std::cout << "X is in the set" << std::endl;
return true;
}
return false;
}

int main(void)
{
is_in_the_set(10); //true
is_in_the_set(std::string("Hello")); //true
is_in_the_set(std::vector<float>{0.1, 0.2}); //false
is_in_the_set(std::vector<double>{12.5, 64.5}); //true
return 0;
}

Generalization:套用到任意 comparison operators

在上面的例子中我們是針對特定的 operator== 做設計,但是如果因為 one_of 實在太方便,我想要實作 one_of 也可以支援其他 operator 我是不是每次都得重頭設計一遍?其實不用,我們只需要連 operator 都當成是一個 template argument 傳進去就行了!因此開始設計ㄅ!

首先,由於 operator== 沒辦法直接當作 argument 傳入 template,因此我先把 operator== 用 struct _op_equal_to 包起來,並在每一個 template 上都加上一個 Fn 的變態:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#include <tuple>
#include <utility>
#include <type_traits>

struct _op_equal_to
{
//TODO
};

template<typename... ArgTypes>
constexpr bool any(ArgTypes&&... args) { return (... || args); }

//TODO
template<typename, typename, typename = std::void_t<>>
struct _whether_support_op : std::false_type
{};

//TODO
template<typename LT, typename RT>
struct _whether_support_op<LT, RT, std::void_t<
decltype(std::declval<LT>()==std::declval<RT>) >> : std::true_type
{};

template<typename Fn, typename LT, typename RT>
constexpr bool __match_comparable_one_of_op(const Fn& op, const LT& lhs, const RT& rhs)
{
//TODO
if constexpr (_whether_support_op<LT, RT>::value)
return lhs==rhs;
else
return false;
}

template<typename Fn, typename T, typename... ArgTypes>
constexpr bool __match_one_of_op(const Fn& op, const T& lhs, ArgTypes&&... args)
{
if constexpr (sizeof...(args) == 0)
return false;
else
return any( (__match_comparable_one_of_op(op, lhs, args)) ...);
}

template<typename... ArgTypes>
struct _type_one_of{
std::tuple<ArgTypes...> args;

template<typename... Ts>
_type_one_of(Ts&&... args): args(std::forward<Ts>(args)...) {}

template<typename Fn, typename T, typename... Ts, std::size_t... I>
constexpr bool __match_op_impl(const Fn& op, const T& lhs, const std::tuple<Ts...> &tup, std::index_sequence<I...>) const
{ return __match_one_of_op(op, lhs, std::get<I>(tup)...); }

template<typename Fn, typename T, typename... Ts, typename Inds = std::make_index_sequence<sizeof...(Ts)>>
constexpr bool __match_op(const Fn& op, const T& lhs, const std::tuple<Ts...> &tup) const
{ return __match_op_impl(op, lhs, tup, Inds{}); }
};

template<typename... ArgTypes>
constexpr auto one_of(ArgTypes&&... args) -> _type_one_of<ArgTypes...>
{
return _type_one_of<ArgTypes...>(std::forward<ArgTypes>(args)...);
}

template<typename T, typename... ArgTypes>
constexpr bool operator==(const T& lhs, const _type_one_of<ArgTypes...> &rhs)
{
return rhs.__match_op(_op_equal_to{}, lhs, rhs.args);
}

有加上 TODO 的都是還沒有完成的部份。首先我們已經可以在 __match_comparable_one_of_op 裏面取得用 struct 包好的 operator 的,接下來就是要設計把 op 也傳入 _whether_support_op 裏面檢查。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template<typename, typename = std::void_t<>>
struct _whether_support_op : std::false_type
{};

template<typename Fn, typename... Ts>
struct _whether_support_op<Fn(Ts...), std::void_t<
decltype(/*TODO*/) >> : std::true_type
{};

template<typename Fn, typename LT, typename RT>
constexpr bool __match_comparable_one_of_op(const Fn& op, const LT& lhs, const RT& rhs)
{
//TODO
if constexpr (_whether_support_op<Fn(LT, RT)>::value)
return lhs==rhs;
else
return false;
}

這邊注意,我們的 specialized template 只剩下兩格,第一個是 Fn(Ts...),第二個是std::void_t<>,因此對應的 primary template 的 typename 格數也要剩下兩格。而 std::void_t<> 的內容物還沒設計。

接下來設計 struct _op_equal_to

1
2
3
4
5
struct _op_equal_to
{
template<typename LT, typename RT>
constexpr auto operator()(const LT& lhs, const RT& rhs) const -> decltype(std::declval<LT&>() == std::declval<RT&>());
};

這邊我使用 overload operator() 來讓 _op_equal_to 變成很像是 function call 的方式來設計,而回傳變態是 decltype(std::declval<LT&>() == std::declval<RT&>()),如果這個東西成立的話,他會變成正確的型態 (通常是 bool)。函式的內容我們不需要定義,剛剛說的,因為我們只會用 decltype 讓 Compiler 檢查 expression 會不會成立而已,我們關心的是那個回傳變態會不會成立,如果成立的話就行了。

接下來就是回到 _whether_support_op 裏面設計 std::void_t<>

1
2
3
4
template<typename Fn, typename... Ts>
struct _whether_support_op<Fn(Ts...), std::void_t<
decltype( std::declval<Fn>()(std::declval<Ts>()...) ) >> : std::true_type
{};

這邊我用 std::declval<Fn>() 產生一個 _op_equal_to 的 reference type 並呼叫他的 member function,傳入的參數是一堆 Ts 型態的 reference type std::declval<Ts>()...

這樣就完成 General 版的 operator supporting 檢查了。

但這邊還有一個問題是,我們真正在比較的地方還沒有 generalize:

1
2
3
4
if constexpr (_whether_support_op<Fn(LT, RT)>::value)
return lhs==rhs; //here
else
return false;

這個地方該怎麼辦?該不會 _op_equal_to 裏面除了有一個虛擬的比較後又要再實作一個單獨的 member function 來做實體的比較?這樣不會太冗嘛?

會。

但是我們只需要動一點手腳就可以做出同時可以虛擬的比較又可以實體的比較的 function 了:
首先比較的部份:

1
2
3
4
if constexpr (_whether_support_op<Fn(LT, RT)>::value)
return op(lhs, rhs); //here
else
return false;

接下來 _op_equal_to 的部份:

1
2
3
4
5
6
struct _op_equal_to
{
template<typename LT, typename RT>
constexpr auto operator()(const LT& lhs, const RT& rhs) const -> decltype(std::declval<LT&>() == std::declval<RT&>())
{ return lhs==rhs; }
};

到這邊就完成了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include <tuple>
#include <utility>
#include <type_traits>

struct _op_equal_to
{
template<typename LT, typename RT>
constexpr auto operator()(const LT& lhs, const RT& rhs) const -> decltype(std::declval<LT&>() == std::declval<RT&>())
{ return lhs==rhs; }
};

template<typename... ArgTypes>
constexpr bool any(ArgTypes&&... args) { return (... || args); }

template<typename, typename = std::void_t<>>
struct _whether_support_op : std::false_type
{};

template<typename Fn, typename... Ts>
struct _whether_support_op<Fn(Ts...), std::void_t<
decltype( std::declval<Fn>()(std::declval<Ts>()...) ) >> : std::true_type
{};

template<typename Fn, typename LT, typename RT>
constexpr bool __match_comparable_one_of_op(const Fn& op, const LT& lhs, const RT& rhs)
{
if constexpr (_whether_support_op<Fn(LT, RT)>::value)
return op(lhs, rhs);
else
return false;
}

template<typename Fn, typename T, typename... ArgTypes>
constexpr bool __match_one_of_op(const Fn& op, const T& lhs, ArgTypes&&... args)
{
if constexpr (sizeof...(args) == 0)
return false;
else
return any( (__match_comparable_one_of_op(op, lhs, args)) ...);
}

template<typename... ArgTypes>
struct _type_one_of{
std::tuple<ArgTypes...> args;

template<typename... Ts>
_type_one_of(Ts&&... args): args(std::forward<Ts>(args)...) {}

template<typename Fn, typename T, typename... Ts, std::size_t... I>
constexpr bool __match_op_impl(const Fn& op, const T& lhs, const std::tuple<Ts...> &tup, std::index_sequence<I...>) const
{ return __match_one_of_op(op, lhs, std::get<I>(tup)...); }

template<typename Fn, typename T, typename... Ts, typename Inds = std::make_index_sequence<sizeof...(Ts)>>
constexpr bool __match_op(const Fn& op, const T& lhs, const std::tuple<Ts...> &tup) const
{ return __match_op_impl(op, lhs, tup, Inds{}); }
};

template<typename... ArgTypes>
constexpr auto one_of(ArgTypes&&... args) -> _type_one_of<ArgTypes...>
{
return _type_one_of<ArgTypes...>(std::forward<ArgTypes>(args)...);
}

template<typename T, typename... ArgTypes>
constexpr bool operator==(const T& lhs, const _type_one_of<ArgTypes...> &rhs)
{
return rhs.__match_op(_op_equal_to{}, lhs, rhs.args);
}

接下來就可以嘗試定義其他 operators

  • operator!= ,這東西沒有什麼好定義的,把 operator== 前面加上 ! 就好了:

    1
    2
    3
    4
    5
    template<typename T, typename... ArgTypes>
    constexpr bool operator!=(const T& lhs, const _type_one_of<ArgTypes...> &rhs)
    {
    return !rhs.__match_op(_op_equal_to{}, lhs, rhs.args);
    }
  • operator<

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    struct _op_less_than
    {
    template<typename LT, typename RT>
    constexpr auto operator()(const LT& lhs, const RT& rhs) const -> decltype(std::declval<LT&>() < std::declval<RT&>())
    { return lhs < rhs; }
    };

    template<typename T, typename... ArgTypes>
    constexpr bool operator<(const T& lhs, const _type_one_of<ArgTypes...> &rhs)
    {
    return rhs.__match_op(_op_less_than{}, lhs, rhs.args);
    }
  • 其他自己試

完整的測試 Code

其他討論

_op_equal_to 的其他寫法

其實 _op_equal_to 這個 struct 還有其他寫法,例如也可以把 decltype 寫到 template 裏面判斷:

1
2
3
4
5
6
struct _op_equal_to
{
template<typename LT, typename RT, typename = decltype(std::declval<LT&>() == std::declval<RT&>())>
constexpr bool operator()(const LT& lhs, const RT& rhs) const
{ return lhs==rhs; }
};

設在 template 的第三個 parameter,然後把 auto 換成 bool。但是我覺得這樣沒有比較好的原因是,lhs==rhs 並沒有保證回傳值一定是 bool。雖然在 comparison 裏面回傳非 bool 值本身就很奇怪。

冗字

後來發現其實有些地方的 std::forward 可以拿掉。

第一個就是 one_of 裏面呼叫 _type_one_of 的 constructor

1
2
3
4
5
template<typename... ArgTypes>
constexpr auto one_of(ArgTypes&&... args) -> _type_one_of<ArgTypes...>
{
return _type_one_of<ArgTypes...>(args...);
}

因為 parameter pack 傳到 parameter pack 直接用 ... unpack 就行了。

第二個是 _type_one_of 的 constructor 裏面呼叫 tuple 的 constructor

1
2
template<typename... Ts>
_type_one_of(Ts&&... args): args(args...) {}

實際上應該還有其他地方可以簡化,只是目前還沒有更多想法。

function type 的 SFINAE

有些人可能會以為要用 std::void_t 才能玩 SFINAE,其實 SFINAE 也不是什麼新概念了,而是因為有了這個概念,才會在 c++ 17 裏面新增 std::void_t 這個東西。在這之前其實也是可以用類似的方法實現 SFINAE,其中一種方式就是用 function 的方式。

這邊示範怎麼用 function type 來寫 SFINAE,首先這是原本 struct type 的 SFINAE

1
2
3
4
5
6
7
8
template<typename, typename = std::void_t<>>
struct _whether_support_op : std::false_type
{};

template<typename Fn, typename... Ts>
struct _whether_support_op<Fn(Ts...), std::void_t<
decltype( std::declval<Fn>()(std::declval<Ts>()...) ) >> : std::true_type
{};

這邊來定義 __whether_support_op function definition (不需要 function 實體):

1
2
3
4
template<typename Fn, typename... Ts, typename = decltype( std::declval<Fn>()(std::declval<Ts>()...) )>
std::true_type __whether_support_op(const Fn&, const Ts&...);

std::false_type __whether_support_op(...);

感覺比 struct type 的 SFINAE 更簡單易懂。

可以看的出來,同樣道理,如果第一個 template function 的第三個 template parameter 成立,我們可以得到 return type 為 std::true_type 的 function,如果不成立,則配對到 return type 為 std::false_type 的 function,然後傳進去的參數就像是垃圾一樣隨便包成一包 parameter pack ...

接著把呼叫的地方改掉:

1
2
3
4
5
6
7
8
template<typename Fn, typename LT, typename RT>
constexpr bool __match_comparable_one_of_op(const Fn& op, const LT& lhs, const RT& rhs)
{
if constexpr (decltype(__whether_support_op(std::declval<Fn>(), std::declval<LT>(), std::declval<RT>()))::value)
return op(lhs, rhs);
else
return false;
}

首先 __whether_support_op 是一個 function,我們可以藉由傳入參數的 reference type std::declval<> 來讓 Compiler 驗證 function。然後用 decltype() 取得 function 的 return type。最後再取出 value 值看看是 true 還是 false。記住!使用 decltype() 呼叫 function,Compiler 不會執行 function 實體,因此我們只需要有 function definition 就好了。

但是這邊我們就使用了一個很醜的方式呼叫我們的 function。實際上我們也可以用漂亮一點的方式,再包一層 SFINAE 的 struct helper:

1
2
3
4
5
template<typename> 
struct _whether_support_op;

template<typename Fn, typename... Ts>
struct _whether_support_op<Fn(Ts...)> : decltype(__whether_support_op(std::declval<Fn>(), std::declval<Ts>()...))

然後呼叫的部份改成原本的:

1
2
3
4
5
6
7
8
template<typename Fn, typename LT, typename RT>
constexpr bool __match_comparable_one_of_op(const Fn& op, const LT& lhs, const RT& rhs)
{
if constexpr (_whether_support_op<Fn(LT, RT)>::value)
return op(lhs, rhs);
else
return false;
}

完整的 function type SFINAE:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
template<typename Fn, typename... Ts, typename = decltype( std::declval<Fn>()(std::declval<Ts>()...) )>
std::true_type __whether_support_op(const Fn&, const Ts&...);

std::false_type __whether_support_op(...);

template<typename>
struct _whether_support_op;

template<typename Fn, typename... Ts>
struct _whether_support_op<Fn(Ts...)> : decltype(__whether_support_op(std::declval<Fn>(), std::declval<Ts>()...))

template<typename Fn, typename LT, typename RT>
constexpr bool __match_comparable_one_of_op(const Fn& op, const LT& lhs, const RT& rhs)
{
if constexpr (_whether_support_op<Fn(LT, RT)>::value)
return op(lhs, rhs);
else
return false;
}

跟單純只用 struct type 的 SFINAE 比起來相對就比較冗一點:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template<typename, typename = std::void_t<>>
struct _whether_support_op : std::false_type
{};

template<typename Fn, typename... Ts>
struct _whether_support_op<Fn(Ts...), std::void_t<decltype( std::declval<Fn>()(std::declval<Ts>()...) ) >> : std::true_type
{};

template<typename Fn, typename LT, typename RT>
constexpr bool __match_comparable_one_of_op(const Fn& op, const LT& lhs, const RT& rhs)
{
if constexpr (_whether_support_op<Fn(LT, RT)>::value)
return op(lhs, rhs);
else
return false;
}

所以我才會使用 struct type 的 SFINAE。

想喝咖啡