C++17使用折疊表達(dá)式實(shí)現(xiàn)一個IsAllTrue函數(shù)的過程
前言
讓我們實(shí)現(xiàn)一個 IsAllTrue 函數(shù),支持變長參數(shù),可傳入多個表達(dá)式,必須全部計(jì)算為true,該函數(shù)才返回true。
本文記錄了逐步實(shí)現(xiàn)與優(yōu)化該函數(shù)的思維鏈,用到了以下現(xiàn)代C++新特性知識,適合對C++進(jìn)階知識有一定了解的人。這樣一種從實(shí)際問題來學(xué)習(xí)和運(yùn)用知識的過程還是挺有趣的,特此整理分享一下。
- 可變長參數(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)
初級版本——基于初始化列表實(shí)現(xiàn)
可以使用初始化列表 std::initializer_list 存儲多個bool變量,實(shí)現(xiàn)傳入多個bool值的目的,這種方法實(shí)際上該函數(shù)只有一個參數(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});這個方法的實(shí)現(xiàn)簡單易用,但是對于代碼有更高追求的人并不滿足于此,以上實(shí)現(xiàn)存在如下問題:
- 傳入?yún)?shù)是一個初始化列表,需要寫大括號{},不夠優(yōu)雅。
- 調(diào)用函數(shù)前計(jì)算了每一個條件表達(dá)式,但實(shí)際任意一個為false,即可返回,可能存在如下問題:
不必要的函數(shù)調(diào)用帶來一定計(jì)算開銷;
當(dāng)前后表達(dá)式存在依賴關(guān)系時,比如
p && p →a,如果p是指針且為空, 計(jì)算p→a會導(dǎo)致程序崩潰。
對于不了解這個函數(shù)用法的人而言,使用這個實(shí)現(xiàn)是會存在一定風(fēng)險的。所以我們需要想辦法利用 && 實(shí)現(xiàn)短路求值,以及對函數(shù)結(jié)果的延遲計(jì)算。
進(jìn)階版本——基于折疊表達(dá)式實(shí)現(xiàn)
折疊表達(dá)式(Fold expressions)

折疊表達(dá)式是C++17引入的新特性,可通過二元操作符折疊可變長參數(shù)模板中的參數(shù)包。這個特性的引入是為了簡化C++11可變長參數(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ù)情況下,對于交換律成立的操作符(如 + 和 *),左折疊和右折疊的結(jié)果是相同的。然而,對于非交換的操作符,結(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á)式的最外層括號是必須的。
但以上實(shí)現(xiàn),該模板本質(zhì)上仍只能支持變長的多個bool參數(shù),這會導(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)用對象、可轉(zhuǎn)換為bool值的指針和數(shù)值。
總體可分為兩類,一類是可轉(zhuǎn)換為bool的表達(dá)式,另一類是可計(jì)算出bool的可調(diào)用對象。
由于參數(shù)類型(bool、函數(shù)對象、指針等)和類型特征(是否可調(diào)用、是否可以轉(zhuǎn)成bool)均是可以在編譯期確定的。
為了避免在編譯期把模板參數(shù)類型都推斷為bool,可定義 IsTrue 函數(shù)模板定義表達(dá)式bool值的計(jì)算方式,使模板可以推斷出原表達(dá)式自身的類型,從而可以延遲其計(jì)算過程。其中用到了編譯期條件if constexpr 和 一種類型萃取是否可調(diào)用 std::is_invocable_v ,這兩個均是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)用對象,調(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個表達(dá)式傳入該模板函數(shù)后,模板實(shí)例化為形同如下形式,從而可以實(shí)現(xiàn)短路機(jī)制:
static_cast<bool>(Expr1) && Expr2() && static_cast<bool>(Expr3) && ... && ExprN()
函數(shù)測試
對以上代碼進(jìn)行如下測試,注釋為輸出結(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); // 無輸出,實(shí)現(xiàn)了短路機(jī)制以及延遲計(jì)算
IsAllTrue(p, p->a); // 正常運(yùn)行,不會coredump以上為了方便,均使用定義了無參lambda函數(shù)進(jìn)行了測試。為了延遲一般含參函數(shù)的計(jì)算結(jié)果,能夠方便傳入帶參數(shù)的函數(shù)對象,還可以基于std::bind實(shí)現(xiàn)一個用于生成可調(diào)用對象的函數(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));完整測試代碼:https://compiler-explorer.com/z/fTvq7Y36Y
知識總結(jié)
本文使用了以下C++知識實(shí)現(xiàn)了一個高效的IsAllTrue函數(shù),優(yōu)點(diǎn)為它的使用安全且較為高效,缺點(diǎn)在于代碼實(shí)現(xiàn)較為復(fù)雜,對C++知識掌握程度要求較高,過多使用也會導(dǎo)致代碼體積膨脹。
- 條件編譯
if constexpr:- 這個關(guān)鍵字用于在編譯時判斷是否滿足條件。如果
T是可調(diào)用對象(例如lambda或函數(shù)對象),則調(diào)用它并返回結(jié)果。 - 如果
T不是可調(diào)用對象,則將其轉(zhuǎn)換為bool。
- 這個關(guān)鍵字用于在編譯時判斷是否滿足條件。如果
- 類型萃取
std::is_invocable_v:- 這是一個用于判斷類型
T是否可調(diào)用的特性。如果T是可調(diào)用對象,則std::is_invocable_v<T>返回true。 - 需要包含 <type_traits> 頭文件
- 這是一個用于判斷類型
- 完美轉(zhuǎn)發(fā)
std::forward:std::forward<T>(value)確保參數(shù)的完美轉(zhuǎn)發(fā),保留其左值或右值性質(zhì)。
- 可變長參數(shù)模板:支持可變數(shù)量的參數(shù)包,語法用
T ... args表示。 - 折疊表達(dá)式
- 使用了C++17中的折疊表達(dá)式 ,它會對參數(shù)從左到右進(jìn)行求值。
- 簡化了可變長參數(shù)模板的使用,提供了一種簡潔而直觀的方式來對參數(shù)包進(jìn)行展開和操作,從而避免了遞歸或顯式循環(huán)的繁瑣。
- 結(jié)構(gòu)化綁定
std::bind:可綁定參數(shù)args到一個函數(shù)f,并返回一個可調(diào)用對象。
參考
到此這篇關(guān)于C++17 使用折疊表達(dá)式實(shí)現(xiàn)一個IsAllTrue函數(shù)的文章就介紹到這了,更多相關(guān)C++ IsAllTrue函數(shù)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C++11標(biāo)準(zhǔn)庫 互斥鎖 <mutex> 詳解
這篇文章主要介紹了C++11標(biāo)準(zhǔn)庫互斥鎖 <mutex> 的相關(guān)知識,使用call_once()的時候,需要一個once_flag作為call_once()的傳入?yún)?shù),本文給大家介紹的非常詳細(xì),感興趣的朋友一起看看吧2024-07-07
C++實(shí)現(xiàn)Dijkstra算法的示例代碼
迪杰斯特拉算法(Dijkstra)是由荷蘭計(jì)算機(jī)科學(xué)家狄克斯特拉于1959年提出的,因此又叫狄克斯特拉算法。是從一個頂點(diǎn)到其余各頂點(diǎn)的最短路徑算法。本文將用C++實(shí)現(xiàn)Dijkstra算法,需要的可以參考一下2022-07-07
C語言實(shí)現(xiàn)數(shù)據(jù)結(jié)構(gòu)和雙向鏈表操作
這篇文章主要介紹了C語言實(shí)現(xiàn)數(shù)據(jù)結(jié)構(gòu)雙向鏈表操作,需要的朋友可以參考下2017-03-03
linux C 打印錯誤信息和標(biāo)準(zhǔn)輸入輸出詳細(xì)介紹
這篇文章主要介紹了linux C 打印錯誤信息和標(biāo)準(zhǔn)輸入輸出詳細(xì)介紹的相關(guān)資料,需要的朋友可以參考下2016-12-12

