Saturday, May 20, 2023

淺談 C++ 的 if constexpr 分支忽略

簡介
以往定義常數,我們都會採用 const 這個關鍵字。雖然它可以確保該常數不變 (immutable),但要注意一點:它並不保證數值是在編譯期間決定的。例如,在賦值的時候,這種寫法是被允許的:

int x = getFromSomewhere();
const int MAX_COUNT = 30 + x;

這樣的話,雖然 MAX_COUNT 是不變的,但我們就不能在編譯期間決定 MAX_COUNT 的數值。這導致程式變得難以除錯,還有就是執行效率難以保證。為了解決這種情況,自 C++11 開始,官方決定引入 constexpr 關鍵字,確保數值必須在編譯期間決定:

constexpr int MAX_COUNT = 30;

這種寫法,雖然會增加編譯的時間,但就避免了程式執行期間的額外開銷,以及降低了日後除錯的難度。而從 C++ 17 開始,為了降低編譯的時間,還有減少程式的體積,constexpr 更開始支援一種新的玩法:只要加上 if 關鍵字,它就能忽略掉指定的分支 (Branch Discard)。

(其實玩法還有很多,但這次就只提及一個最簡單的情況)

---

跟模板 (Template) 一起使用的 if constexpr
有些時候,我們會想用模板寫一個萬能的函式, 然後根據它的資料類型,會有不同的處理方法。以下的例子,是一個可以接受任何類型(T)的函式。當它收到 double 類型,就會執行第一個分支;收到 int 類型,就會執行第二個分支;收到其他類型,就會執行 else 分支:

#include <iostream>

template<typename T>
void testFunction(T objT)
{
    if constexpr (std::is_same_v<T, double>) {

        // Interestingly, compiler ignores template-related syntax:
        objT.function_never_existed();
        T::this_function_does_not_exist_but_it_compiles();

        // However. doing this will still make compilation fail:
        // functionNeverExisted();
    }
    else if constexpr (std::is_same_v<T, int>)
    {
        std::cout << "Integer detected: " << objT << std::endl;
    }
    else
    {
        std::cout << "Else branch for testFunction!" << std::endl;
    }
}

int main(int argc, char* argv[])
{
    testFunction(static_cast<int>(123));
    return 0
}


使用 C++17 或以上,是可以成功編譯的。而程式執行的結果是:

Integer detected: 123

---

解說
這種寫法,如果只使用 C++11 的話,它是根本不能編譯的:

clang++ main.cpp --std=c++11

它會告知 if_same_v 並不存在,還有就是第一分支中,存在不正確的表達式。

但如果使用 C++17 / C++20 的話,就能編譯成功:

clang++ main.cpp --std=c++17
clang++ main.cpp --std=c++2a

事實上,在第一分支中,其實有很多敍述,包括 function_never_existed 是根本不存在的。但它仍然能成功編譯,是因為編譯器一早已經明白,這個 if constexpr 分支內的模板,根本不會被實體化,所以它就會自動把這個部分,與模板相關的表達式全部忽略。但有趣的是,其他非模板的部分,雖然它們最後都會被省略,但在編譯的過程中,仍然會被一一檢查。

所以,如果我們將範例中的 functionNeverExisted() 中的注釋解除,它將會在編譯期間,產生以下的編譯錯誤:

main.cpp:15:3: error: use of undeclared identifier 'functionNeverExisted'
                functionNeverExisted();
                ^
1 error generated.

長話短說就是:C++17 雖然會將分支省略,卻仍不允許在非模板的部分中,有任何違規的寫法。

---

小結
自從 C++17 後,我們可以利用 if constexpr,在編譯期間進行分支忽略,令程式寫法更簡單、執行效率更高,以及可以只寫一個模板函式,就能支援(幾乎所有)不同的資料類型。