潮.C++17 | constexpr if (2) 再會了 enable_if !? 再也不用樣版特化了

TJSW
7 min readMar 1, 2020

--

constexpr if 教學之中,提到了 constexpr if 可以簡單且語意直觀地替代掉 C++ 萬惡的 std::enable_if 所達成的樣版特化選擇。今天就來講講怎麼利用 std::enable_if 來做樣版特化選擇,以及 constexpr if 是如何大幅度改善 enable_if 吧!

std::enable_if 是誰

知道他是誰的同學們可以直接往下一個段落前進。👍🏻

他是 C++ 標準在 C++11 新引入的一個 STL 內建樣版類別。他不是用來創建物件的,而是通過樣板參數去特化不同的成員 typedef

這個樣板類別原型長這樣

template<bool B, typename T>
class enable_if<bool B, T=void>;

顯然是一個非型別的類別樣版 (non-type template class)。當第一個樣版參數B 為 true 時, enable_if<true> 會有一個成員 typedeftype 定義為 T,預設為 void;如果具現化樣版時 B 給的是 false,那就不會有成員 typedef

在 C++11 時同學們可以通過 enable_if<COND>::type 拿到成員 typedef。但 C++14 之後為了讓大家少打點字,多了一個型別別名 (type alias) 叫 enable_if_t<COND> 直接得到和前面一樣的效果。(前提是 COND 結果為 true)

好,關於 enable_if<>大家只要知道這樣就行了。

enable_if 怎麼選擇樣版特化

template<typename T>
void func(T t) { puts("General"); } // (1)
template<>
void func<double>(double) { puts("Hello"); } // (2)
template<>
void func<int>(int) { puts("Hello"); } // (3)

上面給一個簡單的樣版函式,有一個通用的版本 (1),以及對 double 的特化 (2) 還有對 int 的特化 (3)。依照我們呼叫 func() 給的參數會執行 (1) (2) (3) 的其中一種,這個路上找個人應該都懂。目的在於給定 doubleint,我們想要印出 Hello,其它情況想印出 General。

有了 enable_if 我們可以把 (2) 和 (3) 寫在一起

第一段的 is_int_or_double_v<T> 是一個樣版變數,根據給定的型別是否是 intdouble,他就是 true 或 false。

然後兩個 func() 其實就只是長相比較複雜的函式重載 (function overloading) 差別在 is_int_or_double<T> 前有沒有 ! 做 not 的反向邏輯。當我們在 main 寫下呼叫 func(‘a’),此時編譯器看到的兩個 func() 是:

void func(char t, void* = nullptr);
void func(char t, /* ill-formed expression */);

根據 SFINAE 的原則,編譯器選擇第一個 func(),所以印出 General。當呼叫 func(1.1)func(3) 時,就和上面相反,所以選擇第二個 func(),印出 Hello。

以上看來可以說 enable_if 就是個編譯時期的函式版本 switch。但語法十分繁複,還需要藉助預設指標參數來做函式重載騙編譯器,易讀性不那麼高,更嚴重的是發生編譯器報錯時,錯誤訊息簡直慘不忍睹,除非是樣版的老手,不然除錯十分困難。解決這個問題在後來的 C++ 版本有兩大流派:C++17 的 constexpr if 和 C++20 的 concepts。

再會了 enable_if

這邊提的是 constexpr if 的解法:假設各位熟了上一篇對 constexpr 的介紹。那解法就是呼之欲出了。我們想做的就是「編譯期依照型別是否為 intdouble 來編譯選擇的代碼段落」僅此而已。

相比之下代碼簡潔了不知道幾條街。不需要額外的預設參數做函數重載、不需要寫兩次函式的宣告、不需要多餘的 enable_if_t 包裝,簡直一舉數得。

編譯期的費氏數列

有很多作法可以做到編譯期的費氏數列計算,所以我們這邊給一個非型別樣版函式加上 enable_if 的作法。傳統上我們要這樣重載樣版函式:

顯而易見的是需要針對費氏數列的起始項條件作一次重載。第二是要再為了負數項作一次重載。換上 constexpr if 之後,這樣寫就完事了

不需要額外重載兩次 fib(),簡明處理 N 為負數的情況還有起始項,語法非常直觀易讀,讚讚讚。

編譯期型別列表中尋找指定型別

高能警告:以下需要同學對 C++ 11 的 Variadic Template 有一定程度理解,有興趣了解可以先到這裡惡補一下。沒興趣的同學下面就看個 enable_if 和 constexpr if 前後對比就好 lol。

這個範例想要在編譯期判斷給定一個型別,是否是另外給定的多個型別其中一個。

簡單說明,我們將會實作一個不定長度的樣版類 type_list,並且裡頭有個靜態成員樣版函式 typename<typename U> has(),判斷 U 是否在 type_list 的型別列表之中。整個實作大概會長這樣

不熟 variadic template 的同學不用太拘泥在細節,大家只要看到用了 enable_if_t 去做成員函式重載,當 has<U>() 詢問的型別 U 和 parameter pack 的第一項 T 相符就直接回傳 true 不然遞迴下去往後匹配。而且還需對遞迴到最後型別列表都沒有匹配的情況去特化一個 type_list<> 和成員 has() 回傳 false。

使用的方式是這樣的,印出結果應該很直覺能看懂。

使用 constexpr if 之後亂象可以完全改觀

十分直觀地我們把

  1. 詢問的 UT 相等,回傳 true
  2. UT 不相等,回傳遞迴查詢。
  3. 匹配到最後沒有型別時回傳 false。

這三件事情我們用直觀的 if else 邏輯組合就完成表明了。是不是覺得 constexpr if 使用起來實在不管是寫還是讀都更加美侖美奐了。

看了以上幾個例子,完全可以說 constexpr if 可以取代掉 enable_if 九成以上的使用情境 (剩下的一成是小弟菜雞可能沒想到的使用情境)。當然不否認,上面有些簡單的例子用 constexpr function 的特性大概也可以直接很直觀地做完,其實我是故意套進 enable_if SFINAE 的框架中讓大家前後比對 enable_if 和 constexpr if 的差距。

取代之後,寫的代碼量變少了,可以想法更直接地流程表述,可讀性也大大地提高了,對於庫的開發者來說節省很多時間和腦力。對於想了解庫實作的碼農們或新手接承接項目也是降低了大幅度的門檻。

--

--