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