望文生義
C++17 的 constexpr if 和 C++11 引入的 constexpr
有很大程度的關係,所以和 constexpr 不太熟的同學們可以先去看看這篇 C++ constexpr 和 constexpr function 的科普。
主要的語法和用法非常直觀,小學生都看得懂,長這樣
if constexpr (condition) {
// ...
}// 或是加上 elseif constexpr (condition) {
// ...
} else {
// ...
}
意思就是 condition 這個條件需要滿足下列兩條件其中一項:
- 如果在編譯期 (compile-time) 就能算出 true 或 false。
- 或是通過轉型得出 true 或 false。
也就是文件上所說的 contextually converted constant expression of type bool。中文解釋大致上是說他是情境上可轉換為bool
(這是個專有概念),並且轉換為bool
的結果是 constant expression。可以參考這篇的編譯期自訂型別轉換得到更實際的代碼範例。
兩條件其一成立,編譯器便會依照出值 true 或 false 編譯 (而不是傳統的執行) if constexpr
內的區塊內容或是 else
的區塊內容。沒有編譯到的區塊內容可以粗略理解為被編譯器丟棄。
來幾個例子
講完了照本宣科的條文,總該來點 constexpr if 的例子給大家聞香。
我們怎麼在編譯期產生上面所述的滿足兩條件的 condition 呢?顯然編譯期已知的東西包含:樣版型別,樣版非型別參數,或是 constexpr function 的運算結果等等。以下給出幾個範例:
編譯期的 4 bytes 向上取整對齊
假設我們要處理記憶體相關的分配或是回收,又或是檔案系統的 4k page 讀寫。我們需要在編譯期對一些數字向上取整到對齊 4 的倍數… (我知道這比喻很爛)。
輸出結果不需要特別解釋,國中生就能看懂了。這邊想要特別強調兩點:
- 不同的
N
具現化的upround_to_4<N>
運行期並不會有if constexpr
或else
的分支,而是依照具現化的N
是否能被 4 整除而只保留該部份的return
數學四則運算式。 - 這個結果確實是編譯期就得出來的。因為
upround_to_4
是 constexpr function 並且main()
裡面的ur8
, …ur16
也都掛上constexpr
。
通過 g++ a.cpp -S
編譯出組合語言可以發現這些的確是編譯完就算完了。
...
movl $12, -4(%rbp)
movl $12, -8(%rbp)
movl $12, -12(%rbp)
movl $12, -16(%rbp)
movl $16, -20(%rbp)
movl $20, -24(%rbp)
...
既然 constexpr if 在編譯期就能決定要走哪段邏輯的代碼,某種程度上我們也可以把他視為 macro 的替代品,而且與其用 macro 硬要擠在一行面目全非的 #define
,constexpr if 完全就是用一般更人性可讀的程式語言在編譯時期就做好邏輯選擇。
搭配 else if 使用
第二個 constexpr if 的範例是:我們今天想要實作個 get_len 函式,有這兩項功能:
- 如果傳入參數型別的是原生字串
const char*
,那麼回傳長度。 - 如果傳入型別是原生陣列,也回傳長度。
- 如果其它情況,回傳 1。
這個範例可以看到:
- 我們使用 type traits 的
is_same_v<>
判斷一個型別是不是const char*
,並且使用老朋友strlen()
來計算長度。 - 搭配
else if constexpr
使用std::is_array_v<>
判斷型別是不是原生陣列,並使用std::size()
計長度。 - 其它情況就
return 1;
。
不管我們怎麼呼叫這個 get_len()
,任一時間,任一種樣版的具現化,都只會包含其中一個 else if 條件的分支代碼。
同學都有印象小時候老師在黑板上教的是 if
else if
但進了業界之後怎麼會是一個 if
搭配一個 else
就完成業務邏輯呢 ^^,接下來給一個更多邏輯變化的用法。
萬用 vector
產生器 — 嵌套 constexpr if
最後來一個高級操作的範例。高能警告:這個範例需要同學們對 C++17、樣版、type traits 有一定的了解,不太熟稔的同學可以簡單看過不要揪結在細節上。
今天我們想要寫一個 std::vector
產生器,功能是這樣子的:
- 傳入的是個原生一維陣列,把裡頭的值全部做成一個新的 vector 回傳。
- 特別的是:如果這個陣列內含是
const char*
型別,也就是傳入的是一個 C++ 原生字串陣列,為了客戶端方便操作考量,我們做出的vector
想要包含的是std::string
,也就是vector<string>
。 - 傳入的是一個變數,直接把他包成變數型別的
vector
回傳。
這個產生器使用上的效果寫成 code 會是這樣
我們想要這個 make_vector()
函式在編譯時期就決定要怎麼產生我們希望的 vector<>
。依照上面我們寫的邏輯,再加上一點點的 type traits 來輔助,大概可以歸類一下我們要寫的代碼:
為了讓各式個樣的型別都能傳入,而且回傳的型別也不一定,因此make_vector()
必須是個樣版函式,並且回傳值標上 auto
自動推導:
template<typename T>
auto make_vector(T &t)
{}
- 檢查傳入的型別
T
是否為是原生陣列,使用std::is_array_v<T>
來做編譯期的判斷。 - 我們想檢查陣列內含型別是不是
const char*
。所以要先從陣列型別變為內含型別:使用std::remove_extent_t<T>
。再用std::is_same_v
判斷和const char*
一不一樣。一樣的話明確指定構造vector<string>
;不然就交由 vector 的 deduction guide 推導。 - 不是陣列的話就直接包進
std::vector
的 constructor。
備註一下:vector
的 constructor 可以吃兩個 iterator 把其間的內容抄一份來構建 vector
。所以才會有上面 Step 1. 和 Step 2. 傳入 std::begin()
和 std::end()
的用法。
在 main()
裡面,依照傳入不同參數呼叫的 make_vector
,編譯器會具現化不同的 make_vector<T>
:
auto v_val = make_vector(val); // make_vector<int>
auto v_arr = make_vector(arr); // make_vector<int [5]>
auto v_str = make_vector(str); // make_vector<const char *[3]>
然而各個樣版的具現化只會包含符合 constexpr if 條件的分支的代碼。
同學們是不是覺得 C++17 很簡單呢?當然上面這個 make_vector
還不是那麼健壯。為了方便展示大家看 constexpr if 的邏輯,省略很多檢查的情境。對 C++ 夠熟的同學們可以考慮讓 make_vector()
支援傳入右值,或傳入指標、多維度陣列等等的情況…。
小結
個人覺得 constexpr if 就是個給開發者的小福音。
第一點是開發樣版過程中避開 C++11 出現的各種繁複的 type traits 型別特徵,不用再為了不同型別條件去寫各式各樣的樣版特化,利用 SFINAE 加上 std::enable_if
寫得天花亂墜。
第二是,語法上完美地融入了原有習慣的 if
思考邏輯,使用直述式的直觀語法就能做到以前要寫得滿目瘡痍才能做到的事的代碼。
那當然同學們不太需要開發庫或是樣版的話是不用太在意這個新功能啦…
之後會再給同學們帶來 constexpr if 如何取代 std::enable_if
,以及 enable_if
和 SFINAE 的原理。