C++17使用折疊表達(dá)式實(shí)現(xiàn)一個(gè)IsAllTrue函數(shù)的過(guò)程
前言
讓我們實(shí)現(xiàn)一個(gè) IsAllTrue
函數(shù),支持變長(zhǎng)參數(shù),可傳入多個(gè)表達(dá)式,必須全部計(jì)算為true,該函數(shù)才返回true。
本文記錄了逐步實(shí)現(xiàn)與優(yōu)化該函數(shù)的思維鏈,用到了以下現(xiàn)代C++新特性知識(shí),適合對(duì)C++進(jìn)階知識(shí)有一定了解的人。這樣一種從實(shí)際問(wèn)題來(lái)學(xué)習(xí)和運(yùn)用知識(shí)的過(guò)程還是挺有趣的,特此整理分享一下。
- 可變長(zhǎng)參數(shù)模板 (C++11)
- 折疊表達(dá)式 (C++17)
- 條件編譯 if constexpr (C++17)
- 類型萃取 type traits (C++11)
- 完美轉(zhuǎn)發(fā)
std::forward
(C++11) - 結(jié)構(gòu)化綁定
std::bind
(C++11)
初級(jí)版本——基于初始化列表實(shí)現(xiàn)
可以使用初始化列表 std::initializer_list
存儲(chǔ)多個(gè)bool變量,實(shí)現(xiàn)傳入多個(gè)bool值的目的,這種方法實(shí)際上該函數(shù)只有一個(gè)參數(shù),實(shí)現(xiàn)如下:
bool IsAllTrue(const std::initializer_list<bool>& conditions) { return std::all_of(conditions.begin(), conditions.end(), [](const bool a) { return a; }); }
使用方法如下:
int a = 1; bool b = true; auto c = []() {return true;} IsAllTrue({a, b, c});
這個(gè)方法的實(shí)現(xiàn)簡(jiǎn)單易用,但是對(duì)于代碼有更高追求的人并不滿足于此,以上實(shí)現(xiàn)存在如下問(wèn)題:
- 傳入?yún)?shù)是一個(gè)初始化列表,需要寫大括號(hào){},不夠優(yōu)雅。
- 調(diào)用函數(shù)前計(jì)算了每一個(gè)條件表達(dá)式,但實(shí)際任意一個(gè)為false,即可返回,可能存在如下問(wèn)題:
不必要的函數(shù)調(diào)用帶來(lái)一定計(jì)算開(kāi)銷;
當(dāng)前后表達(dá)式存在依賴關(guān)系時(shí),比如
p && p →a
,如果p是指針且為空, 計(jì)算p→a
會(huì)導(dǎo)致程序崩潰。
對(duì)于不了解這個(gè)函數(shù)用法的人而言,使用這個(gè)實(shí)現(xiàn)是會(huì)存在一定風(fēng)險(xiǎn)的。所以我們需要想辦法利用 &&
實(shí)現(xiàn)短路求值,以及對(duì)函數(shù)結(jié)果的延遲計(jì)算。
進(jìn)階版本——基于折疊表達(dá)式實(shí)現(xiàn)
折疊表達(dá)式(Fold expressions)
折疊表達(dá)式是C++17引入的新特性,可通過(guò)二元操作符折疊可變長(zhǎng)參數(shù)模板中的參數(shù)包。這個(gè)特性的引入是為了簡(jiǎn)化C++11可變長(zhǎng)參數(shù)模板的使用。
- 根據(jù)左右方向可分為左折疊和右折疊:
一元左折疊(Unary right fold)和一元右折疊(Unary left fold)形式如下:
( pack op... ) //一元右折疊,從右往左計(jì)算, 等同于(E1 op (... op (EN-1 op EN))) ( ... op pack ) //一元左折疊,從左往右計(jì)算, 等同于(((E1 op E2) op ...) op EN)
在大多數(shù)情況下,對(duì)于交換律成立的操作符(如 +
和 *
),左折疊和右折疊的結(jié)果是相同的。然而,對(duì)于非交換的操作符,結(jié)果可能不同,例如減法或除法。
- 根據(jù)是否有初始值可分為一元和二元:
二元折疊表達(dá)式分為:二元右折疊(Binary right fold)和 二元左折疊(Binary left fold)。
( pack op ... op init ) //二元右折疊 ( init op ... op pack ) //二元左折疊
- 使用二元左折疊的例子
template<typename... Args> void printer(Args&&... args) { ((std::cout<< args << " "), ...)<< "\n"; }
基于一元右折疊的IsAllTrue函數(shù)
基于 &&
運(yùn)算符的一元右折疊(Unary right fold)實(shí)現(xiàn)IsAllTrue如下:
template<typename... Args> bool IsAllTrue(Args... args) { return (std::forward<Args>(args) && ...); }
- 注:折疊表達(dá)式的最外層括號(hào)是必須的。
但以上實(shí)現(xiàn),該模板本質(zhì)上仍只能支持變長(zhǎng)的多個(gè)bool參數(shù),這會(huì)導(dǎo)致先計(jì)算出bool值再傳入,仍未實(shí)現(xiàn)函數(shù)結(jié)果的延遲計(jì)算。
使用type traits 進(jìn)一步優(yōu)化
如何可以實(shí)現(xiàn)延遲計(jì)算呢?首先我們可以明確下,傳遞給該函數(shù)的參數(shù)類型,可能是bool值、可以計(jì)算出bool值的表達(dá)式或可調(diào)用對(duì)象、可轉(zhuǎn)換為bool值的指針和數(shù)值。
總體可分為兩類,一類是可轉(zhuǎn)換為bool的表達(dá)式,另一類是可計(jì)算出bool的可調(diào)用對(duì)象。
由于參數(shù)類型(bool、函數(shù)對(duì)象、指針等)和類型特征(是否可調(diào)用、是否可以轉(zhuǎn)成bool)均是可以在編譯期確定的。
為了避免在編譯期把模板參數(shù)類型都推斷為bool,可定義 IsTrue
函數(shù)模板定義表達(dá)式bool值的計(jì)算方式,使模板可以推斷出原表達(dá)式自身的類型,從而可以延遲其計(jì)算過(guò)程。其中用到了編譯期條件if constexpr
和 一種類型萃取是否可調(diào)用 std::is_invocable_v
,這兩個(gè)均是C++17引入的特性。
如果具備可調(diào)用的特征,則進(jìn)行函數(shù)調(diào)用并返回結(jié)果;否則,將其轉(zhuǎn)換為bool值返回。實(shí)現(xiàn)如下:
template <typename T> bool IsTrue(T&& value) { if constexpr (std::is_invocable_v<T>) { // 如果是可調(diào)用對(duì)象,調(diào)用它并返回結(jié)果 return std::forward<T>(value)(); } else { // 否則,將其轉(zhuǎn)換為bool return static_cast<bool>(std::forward<T>(value)); } }
基于以上模板改寫 IsAllTure
模板函數(shù) :
template <typename... Args> bool IsAllTrue(Args&&... args) { return (IsTrue(std::forward<Args>(args)) && ...); }
該實(shí)現(xiàn)的本質(zhì)是我們希望在用N個(gè)表達(dá)式傳入該模板函數(shù)后,模板實(shí)例化為形同如下形式,從而可以實(shí)現(xiàn)短路機(jī)制:
static_cast<bool>(Expr1) && Expr2() && static_cast<bool>(Expr3) && ... && ExprN()
函數(shù)測(cè)試
對(duì)以上代碼進(jìn)行如下測(cè)試,注釋為輸出結(jié)果,可以看到,能夠滿足我們的需求:
auto lambdaTrue = []() { std::cout<<" lambda true"<<std::endl; return true; }; auto lambdaFalse = []() { std::cout<<" lambda false"<<std::endl; return false; }; class Foo { public: int a; }; Foo* p = nullptr; IsAllTrue(true, lambdaTrue); // 輸出lambda true IsAllTrue(false, lambdaTrue); // 無(wú)輸出,實(shí)現(xiàn)了短路機(jī)制以及延遲計(jì)算 IsAllTrue(p, p->a); // 正常運(yùn)行,不會(huì)coredump
以上為了方便,均使用定義了無(wú)參lambda函數(shù)進(jìn)行了測(cè)試。為了延遲一般含參函數(shù)的計(jì)算結(jié)果,能夠方便傳入帶參數(shù)的函數(shù)對(duì)象,還可以基于std::bind
實(shí)現(xiàn)一個(gè)用于生成可調(diào)用對(duì)象的函數(shù):
template <typename F, typename... Args> auto make_callable(F&& f, Args&&... args) { return std::bind(std::forward<F>(f), std::forward<Args>(args)...); }
CPP 復(fù)制 全屏
比如:
bool less(int a, int b) { return a < b; } IsAllTrue(true, make_callable(less, 1, 2));
完整測(cè)試代碼:https://compiler-explorer.com/z/fTvq7Y36Y
知識(shí)總結(jié)
本文使用了以下C++知識(shí)實(shí)現(xiàn)了一個(gè)高效的IsAllTrue函數(shù),優(yōu)點(diǎn)為它的使用安全且較為高效,缺點(diǎn)在于代碼實(shí)現(xiàn)較為復(fù)雜,對(duì)C++知識(shí)掌握程度要求較高,過(guò)多使用也會(huì)導(dǎo)致代碼體積膨脹。
- 條件編譯
if constexpr
:- 這個(gè)關(guān)鍵字用于在編譯時(shí)判斷是否滿足條件。如果
T
是可調(diào)用對(duì)象(例如lambda
或函數(shù)對(duì)象),則調(diào)用它并返回結(jié)果。 - 如果
T
不是可調(diào)用對(duì)象,則將其轉(zhuǎn)換為bool
。
- 這個(gè)關(guān)鍵字用于在編譯時(shí)判斷是否滿足條件。如果
- 類型萃取
std::is_invocable_v
:- 這是一個(gè)用于判斷類型
T
是否可調(diào)用的特性。如果T
是可調(diào)用對(duì)象,則std::is_invocable_v<T>
返回true
。 - 需要包含 <type_traits> 頭文件
- 這是一個(gè)用于判斷類型
- 完美轉(zhuǎn)發(fā)
std::forward
:std::forward<T>(value)
確保參數(shù)的完美轉(zhuǎn)發(fā),保留其左值或右值性質(zhì)。
- 可變長(zhǎng)參數(shù)模板:支持可變數(shù)量的參數(shù)包,語(yǔ)法用
T ... args
表示。 - 折疊表達(dá)式
- 使用了C++17中的折疊表達(dá)式 ,它會(huì)對(duì)參數(shù)從左到右進(jìn)行求值。
- 簡(jiǎn)化了可變長(zhǎng)參數(shù)模板的使用,提供了一種簡(jiǎn)潔而直觀的方式來(lái)對(duì)參數(shù)包進(jìn)行展開(kāi)和操作,從而避免了遞歸或顯式循環(huán)的繁瑣。
- 結(jié)構(gòu)化綁定
std::bind
:可綁定參數(shù)args到一個(gè)函數(shù)f,并返回一個(gè)可調(diào)用對(duì)象。
參考
到此這篇關(guān)于C++17 使用折疊表達(dá)式實(shí)現(xiàn)一個(gè)IsAllTrue函數(shù)的文章就介紹到這了,更多相關(guān)C++ IsAllTrue函數(shù)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C++11標(biāo)準(zhǔn)庫(kù) 互斥鎖 <mutex> 詳解
這篇文章主要介紹了C++11標(biāo)準(zhǔn)庫(kù)互斥鎖 <mutex> 的相關(guān)知識(shí),使用call_once()的時(shí)候,需要一個(gè)once_flag作為call_once()的傳入?yún)?shù),本文給大家介紹的非常詳細(xì),感興趣的朋友一起看看吧2024-07-07C++實(shí)現(xiàn)Dijkstra算法的示例代碼
迪杰斯特拉算法(Dijkstra)是由荷蘭計(jì)算機(jī)科學(xué)家狄克斯特拉于1959年提出的,因此又叫狄克斯特拉算法。是從一個(gè)頂點(diǎn)到其余各頂點(diǎn)的最短路徑算法。本文將用C++實(shí)現(xiàn)Dijkstra算法,需要的可以參考一下2022-07-07C語(yǔ)言實(shí)現(xiàn)數(shù)據(jù)結(jié)構(gòu)和雙向鏈表操作
這篇文章主要介紹了C語(yǔ)言實(shí)現(xiàn)數(shù)據(jù)結(jié)構(gòu)雙向鏈表操作,需要的朋友可以參考下2017-03-03C語(yǔ)言實(shí)現(xiàn)簡(jiǎn)易五子棋小游戲
這篇文章主要為大家詳細(xì)介紹了C語(yǔ)言實(shí)現(xiàn)簡(jiǎn)單五子棋小游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-05-05linux C 打印錯(cuò)誤信息和標(biāo)準(zhǔn)輸入輸出詳細(xì)介紹
這篇文章主要介紹了linux C 打印錯(cuò)誤信息和標(biāo)準(zhǔn)輸入輸出詳細(xì)介紹的相關(guān)資料,需要的朋友可以參考下2016-12-12用C語(yǔ)言簡(jiǎn)單實(shí)現(xiàn)掃雷小游戲
這篇文章主要為大家詳細(xì)介紹了用C語(yǔ)言簡(jiǎn)單實(shí)現(xiàn)掃雷小游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-08-08