C++ 中的 if-constexpr語法和作用
1 if-constexpr 語法
1.1 基本語法
? if-constexpr 語法是 C++ 17 引入的新語法特性,也被稱為常量 if 表達(dá)式或靜態(tài) if(static if)。引入這個語言特性的目的是將 C++ 在編譯期計算和求值的能力進(jìn)一步擴展,更方便地實現(xiàn)編譯期的分支選擇動作。早期的 C++ 缺少類似的語言特性,C++ 開發(fā)者不得不使用 tag dispatching 這樣的慣用手法或者借助于模板特化機制讓編譯器在模板參數(shù)推導(dǎo)時“曲折地”實現(xiàn)一些靜態(tài)選擇。C++ 11 標(biāo)準(zhǔn)明確了 SFINAE 機制的應(yīng)用,并提供了 enable_if
,二者配合可以更方便地實現(xiàn)編譯期的分支選擇,但是與 if-constexpr 相比,可讀性和易用性都差了幾條街。
? 在介紹 if-constexpr 的“神通”之前,前來看一下 if-constexpr 的語法形式。其實我們在介紹如何讓自己的設(shè)計的類型支持結(jié)構(gòu)化綁定的時候(《讓自定義類型支持結(jié)構(gòu)化綁定》),為實現(xiàn) get<N>()
成員方法,就用到了這種語法,這里再回顧一下 if-constexpr 語法的表達(dá)形式:
struct FooTest { template<std::size_t N> const auto& get() const { if constexpr (N == 0) return name; else if constexpr (N == 1) return age; //else if 分支中的 constexpr 說明符可以省略 else return weight; } };
這個函數(shù)中對模板參數(shù) N 的判斷在編譯期間進(jìn)行,由哪個分支返回數(shù)據(jù)也是在編譯期間就決定了,false 分支中的代碼甚至不會出現(xiàn)在實例化后的函數(shù)代碼中。以get<N>()
成員函數(shù)為例,如果使用 FooTest 的代碼用到 get<0>()
取 name 這個成員,則最終實例化后會得get<0>()
的特化實現(xiàn):
struct FooTest { template<> const std::string& get<0>() const { return name; } };
如果代碼中還用了get<1>()
取 age 的值,則編譯器還會實例化出 get<1>()
這個特化版本:
template<> const int& get<1>() const { return age; }
1.2 擴展說明
1.2.1 條件表達(dá)式
? 常量 if 表達(dá)式中的條件表達(dá)式必須是 bool 類型的常量表達(dá)式,或者是可轉(zhuǎn)換成 bool 類型的常量表達(dá)式。因為這個條件表達(dá)式是在編譯期進(jìn)行評估的,所以 constexpr 修飾的常量或函數(shù)都可以出現(xiàn)在這個表達(dá)式中,但是運行期間的變量或非 constexpr 的函數(shù)不可以出現(xiàn)在條件表達(dá)式中。
? C++ 17 中,lambda 表達(dá)式(lambda 函數(shù))可以被顯式聲明為 constexpr,但是當(dāng)一個 lambda 表達(dá)式和相關(guān)的上下文一起達(dá)成一個閉包時,如果這個閉包在一個 constexpr 上下文中使用,即使沒有將其顯式聲明為 constexpr,它也會被當(dāng)作 constexpr 使用,比如:
auto DoubleIt = [](int n) { return n + n; }; template<std::size_t N> bool Func2() { if constexpr (DoubleIt(N) < 100) return true; else return false; } std::cout << Func2<10>() << ", " << Func2<50>() << std::endl; //1, 0
1.2.2 false 分支處理
? 函數(shù)模板實例化時,評估為 false 分支的語句不會出現(xiàn)在最終實例化后的函數(shù)代碼中,但是編譯器會對其進(jìn)行語法檢查,當(dāng)出現(xiàn)語法錯誤時也會報錯(有些編譯器報錯)。雖然會進(jìn)行語法檢查,但是 false 分支中的 return 語句不會參與函數(shù)的返回值類型推定,比如這個例子:
template<typename T> auto get_value(T t) { if constexpr (std::is_pointer_v<T>) return *t; else return t; }
當(dāng)std::is_pointer_v<T>
為 true 時,else 分支中的 return t 語句不參與返回值類型推定,函數(shù)返回值類型推定為 *t
的類型。當(dāng)std::is_pointer_v<T>
為 false 時正好相反,else 分支中的 return t 語句將決定返回值類型,函數(shù)返回值類型推定為 t 的類型。這里需要注意,如果取消配合 if-constexpr 的 else 分支,改用函數(shù)設(shè)計常用的默認(rèn)返回方式,編譯器就會報錯,比如:
template<typename T> auto get_value(T t) { if constexpr (std::is_pointer_v<T>) return *t; return t; }
這個 get_value2() 函數(shù)的代碼語義與前面的 get_value() 函數(shù)一樣,但是編譯器會報錯,因為最后的 return 語句也參與返回值類型的推定,并且當(dāng) T 是指針類型的時候,兩個 return 語句的返回值類型推定會互相矛盾。
? 盡管編譯器會對 false 分支的代碼進(jìn)行語法檢查,但是 false 分支的代碼會被丟棄,所以不參與代碼鏈接。比如這個例子:
extern int x; // int f() { if constexpr (true) return 0; else if (x) return x; else return -x; } f(); //調(diào)用 f
盡管全局變量 x 只有一個 extern 聲明,并沒有定義,但是這段代碼編譯正常,沒有錯誤。因為 else if 和 else 分支的代碼都被丟棄,編譯器沒有為鏈接代碼而定位 x 的需要。
? 編譯器之所以要對準(zhǔn)備丟棄的 false 分支代碼進(jìn)行語法檢查,可能原因是它要對整個 if 語句進(jìn)行分析,了解每個分支邏輯的起始位置和結(jié)束位置,以便能夠正確地保留 true 分支的代碼,丟棄 false 分支的代碼。
1.2.3 初始化語句
? C++ 17 開始支持在 if 語句中使用初始化語句,當(dāng)然,if-constexpr 語法上也支持初始化語句,只是要求初始化語句也只能使用常量和常量表達(dá)式。比如這個例子中的 k 的初始化:
template<std::size_t N> bool Func() { if constexpr (constexpr std::size_t k = 3; (N % k) == 0) return true; else return false; }
k 必須是個常量,初始化 k 的表達(dá)式必須是常量表達(dá)式。
2 if-constexpr 的作用
? 可在編譯期執(zhí)行的 if-constexpr,用于模板元編程中的條件判斷,不僅擴展了模板元編程的分支處理能力,也簡化了很多以前用非常復(fù)雜方式實現(xiàn)的分支判斷代碼,使得模板元編程對條件分支的處理代碼更直觀,更容易理解。這一部分我們用三個例子,分別介紹一下 if-constexpr 的作用,包括對傳統(tǒng)的 tag dispatching 和模板特化習(xí)慣用法的比較。
2.1 簡化可變參數(shù)的處理方式
? 使用 if-constexpr 可以提高泛型代碼的可讀性,上一節(jié)介紹的 get<N>()
的函數(shù),如果不用 if-constexpr,就需要借助于模板的遞歸推導(dǎo)來解決 N 的個數(shù)不確定問題。具體做法就是定義一個泛化版本加上一個特化版本,這樣實現(xiàn)起來不僅麻煩,代碼可讀性也大打折扣。這一節(jié),我們用一個之前介紹過的用于求和的函數(shù)模板為例,介紹一下 if-constexpr 對可變參數(shù)的處理以及提高代碼可讀性能帶來的好處。
? 在 C++ 17 之前,沒有折疊表達(dá)式和 if-constexpr,對參數(shù)包的處理需要用到模板的遞歸推導(dǎo),需要定義一個結(jié)束遞歸推導(dǎo)的特化實例,非常不直觀:
template<typename T> auto Sum(T arg) { return arg; } template<typename T, typename... Args> auto Sum(T arg, Args... args_left) { return arg + Sum(args_left...); } std::cout << Sum(3, 5, 8) << std::endl; //輸出16 std::cout << Sum(std::string("Emma "), std::string(" love cats!")) << std::endl; //輸出 Emma love cats!
C++ 17 引入了折疊表達(dá)式,用了折疊表達(dá)式就簡單多了,看看折疊表達(dá)式的版本:
template<typename... Args> auto Sum(Args&&... args) { return (0 + ... + args); }
但是折疊表達(dá)式的語法讓很多初學(xué)者“毛骨悚然”,如果改成用 if-constexpr 實現(xiàn),則代碼更符合直覺,可讀性也上了一個臺階:
template <typename T, typename... Args> auto Sum(T arg, Args... args) { if constexpr (0 == sizeof...(Args)) return arg; else return arg + Sum(args...); }
2.2 比std::enable_if 更靈活
? SFINAE (Substitution Failure Is Not An Error)的意思就是模板推導(dǎo)的過程中,如果模板參數(shù)替換后得到一個無意義的錯誤結(jié)果,編譯器并不立即報錯,而是暫時忽略這個模板函數(shù)聲明,繼續(xù)參數(shù)推導(dǎo)和替換。C++ 11 引入的 std::enable_if 就是實現(xiàn) SFINAE 的最直接方式,下面用 ToString() 函數(shù)為例(注意,這不是一個嚴(yán)謹(jǐn)?shù)膶崿F(xiàn),只是作為一個例子),看看如何用 std::enable_if 實現(xiàn)編譯期的條件分支。
//也可以用 enable_if_t template<typename T> std::enable_if<std::is_arithmetic<T>::value, std::string>::type ToString(T t) { return std::to_string(t); } template<typename T> std::enable_if<!std::is_arithmetic<T>::value, std::string>::type ToString(T t) { return t; }
std::to_string() 支持將一個整數(shù)型數(shù)據(jù)或浮點數(shù)型數(shù)據(jù)轉(zhuǎn)成字符串,如果 T 本身就是 std::string 類型,則不需要轉(zhuǎn)換。std::enable_if 的作用就是通過對返回值類型的控制,使得當(dāng)類型 T 與函數(shù)代碼不匹配(比如 to_string() 函數(shù)不支持 std::string 類型)的時候產(chǎn)生一個錯誤的函數(shù)聲明。舉個例子,當(dāng) T 是 std::string 類型時(不是數(shù)字類型),編譯器對兩個模板函數(shù)進(jìn)行參數(shù)替換后得到兩個函數(shù)聲明:
template<> ToString<std::string>(std::string t); template<> std::string ToString<std::string>(std::string t);
第一個替換結(jié)果沒有函數(shù)返回值,是個語法上錯誤的函數(shù)聲明,編譯器會丟棄這個替換結(jié)果,選擇第二個語法上正確的作為最終的 ToString() 函數(shù)重載裁決結(jié)果。如此一來,就通過 std::enable_if 與 SFINAE 機制配合,實現(xiàn)了編譯期分支選擇的目的。
? 但是使用 std::enable_if 控制需要注意一點,std::enable_if 只能將條件分割成兩種情況,就是兩個條件必須互斥,即一個是 true 條件,另一個必須是 false 條件,否則一旦出現(xiàn)兩個判斷條件都是 true 的情況,就會出現(xiàn)兩個正確的結(jié)果,導(dǎo)致編譯器報告“模棱兩可的函數(shù)調(diào)用” 的編譯錯誤。通過上面的例子可以看出,盡管 std::enable_if 也能實現(xiàn)編譯期的條件分支選擇,但是代碼并不直觀,約束條件比較多,且只能實現(xiàn)兩個分支的選擇?,F(xiàn)在看看使用 if-constexpr 的實現(xiàn)方案:
template<typename T> auto ToString(T t) { if constexpr (std::is_arithmetic<T>::value) return std::to_string(t); else return t; }
這樣的代碼要比寫兩個重載函數(shù)讓編譯器按照 SFINAE 原則匹配調(diào)用的方式更直觀,也更容易理解和維護。
2.3 比 tag dispatching 更直觀
? tag 就是一些沒有數(shù)據(jù),沒有操作的空類型,它們可以作為函數(shù)參數(shù)來影響編譯器對重載函數(shù)的選擇。用 tag dispatching 技術(shù)首先要定義 tag,根據(jù)本文的例子,我們定義兩個 tag:
struct NumTag {}; struct StrTag {};
接著要定義重載函數(shù),唯一不同的參數(shù)就是 tag 類型,tag 類型作為函數(shù)的啞形參只影響編譯器對重載函數(shù)的選擇,最終這個沒有的參數(shù)都會被編譯器優(yōu)化掉:
template <typename T> auto ToString_impl(T t, NumTag) { return std::to_string(t); } template <typename T> auto ToString_impl(T t, StrTag) { return t; }
最后就是實現(xiàn) ToString(),根據(jù) T 的類型確定是調(diào)用 ToString_impl(t, NumTag()); 還是調(diào)用 ToString_impl(t, StrTag());,具體做起來就八仙過海,各顯神通,比如這個使用自定義 type_traits 的方式:
template <typename T> //一個并不嚴(yán)謹(jǐn)?shù)姆夯姹? struct traits { typedef NumTag tag; }; template <> //針對 std::string 的特化版本 struct traits<std::string> { typedef StrTag tag; }; template <typename T> auto ToString(T t) { return ToString_impl(t, typename traits<T>::tag()); //根據(jù) traits<T>::tag 選擇 ToString_impl() }
? 對比上一節(jié)用 if constexpr 實現(xiàn)的版本,可以看出來使用 tag dispatching 的代碼比較晦澀,需要研究一下 tag 的定義才能了解分支選擇的具體條件,代碼實現(xiàn)不如 if constexpr 直觀。
3 if-constexpr 與 if 的區(qū)別
3.1 if 為什么不行
? 上一節(jié)的 ToString() 函數(shù)的例子如果不用 if-constexpr,像這樣直接用 if 實現(xiàn):
template<typename T> auto ToString(T t) { if (std::is_arithmetic<T>::value) return std::to_string(t); else return t; }
是否也可以呢?答案是不可以,因為 std::is_arithmetic<T>
是在編譯期求值的,當(dāng)代碼中需要將整數(shù) 42 轉(zhuǎn)成字符串,調(diào)用 ToString(42) 的時候,傳入?yún)?shù)是 int 或 double,評估結(jié)果是 true,此時函數(shù)模板被實例化成:
auto ToString(int t) { if (true) return std::to_string(t); else return t; }
這個實例化結(jié)果是無法編譯的,因為返回值到底是整數(shù)還是 std::string 呢?兩個 return 語句的返回值類型不一致。再看看到傳入?yún)?shù)是 std::string 的情況,此時 if 的評估結(jié)果是 false,函數(shù)模板被實例化成:
auto ToString(std::string t) { if (false) return std::to_string(t); else return t; }
盡管走 else 分支,直接返回 t 沒有問題,但是 if 分支的編譯會有問題,因為 std::to_string() 不支持 std::string 類型。所以,直接使用 if 語句是不可以的。
3.2 if-constexpr 為什么可以
? 現(xiàn)在對比使用 if-constexpr 的情況。前面提到過,對于 false 分支編譯只進(jìn)行語法分析,不生成代碼。所以當(dāng)代碼中出現(xiàn) ToString(42) 的調(diào)用的時候,傳入?yún)?shù)是 int,評估結(jié)果是 true,此時函數(shù)模板被實例化成:
std::string ToString(int t) { return std::to_string(t); }
當(dāng)傳入?yún)?shù)是字符串類型的時候,else 分支就成為 true 分支被保留,函數(shù)模板實例化的結(jié)果就是:
std::string ToString(std::string t) { return t; }
最終實例化的結(jié)果和使用 std::enable_if 的結(jié)果是一樣的,但是語法比 std::enable_if 簡單,直觀。
4 if-constexpr 與 #if 的區(qū)別
? 編譯期 if 表達(dá)式很容易讓人想到 C++ 的條件編譯指令 #if,但是它們的區(qū)別還是很明顯的,主要有三點:
- 處理階段不同:#if 條件編譯指令是在代碼預(yù)處理階段解析的,預(yù)處理器處理完成后提交給編譯器時,編譯器只能看到 true 分支的內(nèi)容,而 if-constexpr 的代碼都是在編譯階段進(jìn)行處理的;
- 條件表達(dá)式要求不同:首先是代碼處理的階段不一樣,#if 只能使用用于定義的宏、編譯器預(yù)定義的宏和環(huán)境變量,不能使用代碼中的函數(shù)或變量,而 if-constexpr 的條件表達(dá)式可以是代碼中的常量,或者常量函數(shù);
- false 分支的處理方式不同:條件編譯中的 false 分支編譯器不進(jìn)行語法檢查,實際上它們在預(yù)編譯階段就被過濾掉了,而 if-constexpr 中的 false 分支也進(jìn)行語法檢查。
到此這篇關(guān)于C++ 中的 if-constexpr的文章就介紹到這了,更多相關(guān)C++ if-constexpr內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
c++將數(shù)組名作為函數(shù)參數(shù)對數(shù)組元素進(jìn)行相應(yīng)的運算
這篇文章主要介紹了c++將數(shù)組名作為函數(shù)參數(shù)對數(shù)組元素進(jìn)行相應(yīng)的運算,需要的朋友可以參考下2014-05-05最新VScode C/C++ 環(huán)境配置的詳細(xì)教程
這篇文章主要介紹了最新VScode C/C++ 環(huán)境配置的詳細(xì)教程,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-11-11