泛型Lambda,如此強(qiáng)大!

1
泛型編程
開(kāi)始之前,先來(lái)簡(jiǎn)單回顧一下泛型編程的內(nèi)容。
泛型編程的目的是將「數(shù)據(jù)和方法」進(jìn)行分離,將數(shù)據(jù)高度抽象,于是可以表示同類問(wèn)題的「最小通解」。
C++中,通過(guò)模板來(lái)實(shí)現(xiàn)泛型編程,模板又分為變量模板、函數(shù)模板和類模板。
這些模板始終圍繞著「數(shù)據(jù)和方法」。變量模板屬于對(duì)數(shù)據(jù)類型的抽象,函數(shù)模板屬于對(duì)方法的抽象,而類模板,則二者兼有,因?yàn)轭惐旧淼哪康木褪菍?shù)據(jù)和方法進(jìn)行結(jié)合。
因此為什么說(shuō)函數(shù)模板處理的是數(shù)值,而類模板處理的是類型呢?就是由于函數(shù)只具有方法,而在C++中方法是不支持偏特化的,所以它無(wú)法處理類型。
到了C++14,Lambda也迎來(lái)了泛化能力,稱為Generic Lambda。不過(guò)此時(shí)的泛化能力只是由auto帶來(lái)的,威力略弱。
隨后又經(jīng)過(guò)多年的發(fā)展,Lambda的能力越來(lái)越強(qiáng)。C++20加入了Template Lambda,這讓Lambda也可以指定模板參數(shù),使得Lambda的泛化能力更加完善。
至此,C++的泛型編程多了一個(gè)新的主角——泛型Lambda。
2
泛型Lambda
為何泛型Lambda值得單獨(dú)拿出來(lái)說(shuō)呢?
一是因其特殊性,在一些情境使用它來(lái)封裝變化,會(huì)讓事情簡(jiǎn)單許多;二是由其新穎性,它的許多特性和用處尚處探索期,值得討論。
首先來(lái)說(shuō)其特殊性。
Lambda函數(shù)其實(shí)就是一個(gè)匿名的函數(shù)對(duì)象,它實(shí)際上也是一個(gè)”類”。不同的是,它唯一的方法就是operator(),也就是Lambda體,而數(shù)據(jù)則是[]中捕獲的參數(shù),這些參數(shù)就是”類”中定義的成員變量。
因此,Lambda函數(shù)既具有函數(shù)的部分特征,又具有類的部分特征。
也因如此,事情變得有趣起來(lái)。
Lambda具有的函數(shù)部分特征,讓它具備了函數(shù)模板的能力;類部分特征,讓它具備了類模板的繼承能力。
此外,由于Lambda的類型是一個(gè)closure type(閉包類型),所以它還可以定義在函數(shù)內(nèi)部,也可以當(dāng)作回調(diào)函數(shù)使用。
如此這些,再加上泛型,使得泛型Lambda極具威力。
繼而來(lái)看其新穎性。
當(dāng)下大多數(shù)C++開(kāi)發(fā)者對(duì)于Lambda的使用,還只是停留在函數(shù)部分,相當(dāng)于只發(fā)揮了Lambda的基本能力。
實(shí)際上,Lambda的能力要比想象之中強(qiáng)大許多,在基本能力之上,還有些令人興奮的能力。
這也是值得探索的地方。
3
以繼承封裝「變化」
不論寫(xiě)庫(kù)或框架,都是在提煉「不變」的邏輯,將「變化」的邏輯交給用戶配置。
可以是預(yù)留接口,讓用戶覆寫(xiě)接口;也可以是采用回調(diào),讓用戶提供處理邏輯;抑或是提供配置文件,讓用戶填寫(xiě)變化的信息,再通過(guò)配置文件自動(dòng)生成相應(yīng)處理邏輯。
應(yīng)對(duì)「變化」的方式很多,對(duì)于一些邏輯不甚復(fù)雜的變化,完全可以借助Lambda來(lái)實(shí)現(xiàn)。
Lambda天生可以在函數(shù)內(nèi)部構(gòu)建,自帶一個(gè)operator (),這就相當(dāng)于一個(gè)表示變化的接口,也就是用戶可以手動(dòng)配置的地方。
有了表示變化的地方,你再將不變的邏輯封裝到一個(gè)類中,讓該類繼承自此Lambda。于是,你便可以在不變之中使用變化的邏輯。
4
Lambda重載
既然泛型Lambda具備函數(shù)模板的特性,那么它是否也可以重載呢?
回答是no。前文提到,Lambda是函數(shù)對(duì)象,它只有唯一的一個(gè)方法operator(),也就是Lambda體,Lambda體只有一個(gè),你又如何能寫(xiě)多個(gè)呢?
但是,可以提供多個(gè)Lambda,也就是造就多個(gè)函數(shù)對(duì)象,讓它們參數(shù)不同,再借助某種技巧,便可以從「視覺(jué)層面」實(shí)現(xiàn)Lambda重載。
說(shuō)是「視覺(jué)層面」,意思是說(shuō)它本質(zhì)上不是函數(shù)意義上的那種重載,只是使用起來(lái)像是函數(shù)重載一樣。
這個(gè)技巧就是overload pattern。
其實(shí)早在C++ DP.13-2 泛化實(shí)現(xiàn)Cyclic Visitor與強(qiáng)大的C++17 std::visit這篇文章中,就已經(jīng)提到并使用了這個(gè)技巧。
這里再次拿出一節(jié)來(lái)介紹它,是因?yàn)槲野l(fā)現(xiàn)它比想象之中更加強(qiáng)大,可以說(shuō)是泛型Lambda編程的一個(gè)核心技術(shù)。
它的實(shí)作很簡(jiǎn)單,只有兩行代碼:
1template<class...?Ts>?struct?overloaded?:?Ts...?{?using?Ts::operator()...;?};
2template<class...?Ts>?overloaded(Ts...)?->?overloaded;
各位都知道,C++中代碼越少往往并不意味著它有多簡(jiǎn)單,而是說(shuō)明其「信息密度」較大。
此處,第一行首先使用了可變參數(shù)模板,使得overloaded可以繼承自多個(gè)Lambda。其次使用了Using-declaration,以防止重載之時(shí)產(chǎn)生歧義。
第二行則使用了C++17的CTAD(Class Template Argument Deduction),以推導(dǎo)出overloaded的類型。有何必要呢?這是因?yàn)槟銦o(wú)法創(chuàng)建一個(gè)overloaded類型的對(duì)象,因?yàn)長(zhǎng)ambda的類型不可知,你無(wú)法填寫(xiě)模板參數(shù)類型。借由CTAD,便可以為overloaded添加一個(gè)用戶自定義的類型推導(dǎo)指引,這樣編譯器才能夠推導(dǎo)出其類型。
現(xiàn)在,就可以使用「視覺(jué)層面」的Lambda重載了:
1const?auto?func?=?overloaded?{
2????[](const?int&?n)?{?std::cout?<"int:"?<'\n';?},\
3????[](const?std::string&?s)?{?std::cout?<"string:"?<'\n';?}
4};
5
6func(2);
7func("im?the?lambda?with?parameter?std::string");
這里又使用了「聚合初始化」,通過(guò)它可以直接調(diào)用基類中Lambda的構(gòu)造器,從而避免為overloaded顯式編寫(xiě)構(gòu)造函數(shù)向基類傳遞參數(shù)。
總而言之,通過(guò)Lambda重載,便可以將許多相似的「變化邏輯」聚到一起,再以不同的參數(shù)訪問(wèn)這些不同的邏輯,從而以一種嶄新的形式封裝變化。
5
泛型Lambda實(shí)現(xiàn)對(duì)象工廠
這一節(jié)需要你對(duì)C++ DP.08 Factory Method這篇文章有些印象。
通過(guò)泛型Lambda,我們擁有了一種新的實(shí)現(xiàn)對(duì)象工廠的策略,簡(jiǎn)單而威力巨大。
代碼如下:
1template<class...?Ts>?struct?Fruit?:?Ts...?{?using?Ts::operator()...;?};
2template<class...?Ts>?Fruit(Ts...)?->?Fruit;
是的,就是使用了Lambda重載來(lái)實(shí)現(xiàn)Fruit。
然后再通過(guò)以下形式定義對(duì)象工廠:
1struct?Apple?{?void?print()?{?std::cout?<"apple?print\n";?}?};
2struct?Pineapplce?{void?print()?{?std::cout?<"pineapple?print\n";?}?};
3
4//?定義對(duì)象工廠
5static?constexpr?auto?FruitFactory?=?Fruit?{
6????[]<typename?T>(const?T&?apple)?{?return?new?T;?}
7};
8
9//?從工廠創(chuàng)建產(chǎn)品
10auto?apple?=?FruitFactory(Apple{});
11apple->print();
此處第6行代碼便使用了C++20的Template Lambda,由此我們可以創(chuàng)建任意類型的對(duì)象。
如此少的代碼,實(shí)現(xiàn)的對(duì)象工廠可并不弱,而且這種實(shí)現(xiàn)方法更加輕便,除了無(wú)法動(dòng)態(tài)產(chǎn)生,已經(jīng)相當(dāng)不錯(cuò)了。
6
泛型Lambda實(shí)現(xiàn)抽象工廠
這一節(jié)需要你對(duì)C++ DP.09 Abstract Factory這篇文章有些印象。
沒(méi)錯(cuò),根據(jù)泛型Lambda,實(shí)現(xiàn)抽象工廠也有了一種新的形式。
并且這種形式使用起來(lái)更加輕便,我已經(jīng)決定使用這種方式替換okdp中的實(shí)現(xiàn)。
我們可以通過(guò)Lambda重載來(lái)定義抽象工廠:
1template<class...?Ts>?struct?AbstractAIFactory?:?Ts...?{?using?Ts::operator()...;?};
2template<class...?Ts>?AbstractAIFactory(Ts...)?->?AbstractAIFactory;
具體工廠的定義則更具有技巧性,實(shí)現(xiàn)如下:
1template<class?T,?class?U>
2concept?IsAbstractAI?=?std::same_as;
3
4template<class?T>
5static?constexpr?auto?AIFactory?=?AbstractAIFactory?{
6????[]()?requires?IsAbstractAI?{?return?new?LuxEasy;?},
7????[]()?requires?IsAbstractAI?{?return?new?ZiggsEasy;?},
8????[]()?requires?IsAbstractAI?{?return?new?TeemoEasy;?}
9};
10
11auto?lux?=?AIFactory();
12lux->print();
你是否意識(shí)到了這種實(shí)現(xiàn)形式的強(qiáng)大之處?
這里用到的技術(shù)就更加多了,除了前面介紹的「聚合初始化」,還使用到了C++20的Concepts,這點(diǎn)我們已經(jīng)寫(xiě)過(guò)文章了,相信大家不會(huì)太陌生。
此外,這里還用到了「變量模板」,想想前面幾節(jié)的內(nèi)容,提到過(guò)范型Lambda雖然具有函數(shù)模板和類模板的部分特征,但它的「數(shù)據(jù)」部分只能通過(guò)捕獲參數(shù)。因此其實(shí)無(wú)法真正像類那樣使用,而抽象工廠的抽象類又無(wú)法實(shí)例化,所以我們也無(wú)法像對(duì)象工廠那樣使用。
于是,為了為它添上「類型的能力」,這里借助了變量模板。正因如此,你才能像類一樣使用AIFactory。
不過(guò)事情尚未結(jié)束,此時(shí)「抽象工廠」就是AbstractAIFactory,通過(guò)Lambda重載完成的不錯(cuò)。「具體工廠」屬于變化的部分,就相當(dāng)于Lambda體,也就是這里為每個(gè)類型實(shí)現(xiàn)的Lambda函數(shù)。
問(wèn)題在哪呢?巨大的重復(fù)!
消除這種類型的重復(fù)比較好的方法是借助「泛型宏」,這點(diǎn)在對(duì)象工廠那篇文章中介紹并使用過(guò)。
泛型編程是理念,模板是手段,宏同樣是一種手段,需要根據(jù)具體情形具體分析,從而合理地進(jìn)行選擇。
不過(guò)此處的情形有些復(fù)雜,泛型宏的確可以很好的完成任務(wù),但是工作比較復(fù)雜,已經(jīng)涉及到泛型宏深入層次的技術(shù)了。
因此由于本篇主題不是泛型宏,篇幅有限,此處只展示下代碼,不做進(jìn)一步解釋,大家可以自己看看。
代碼如下:
#define?_GET_OVERRIDE(_1,?_2,?_3,?_4,?_5,?_6,?NAME,?...)?NAME
#define?_CONCRETE_AI_FACTORY_BODY_0(LAM,?AIType,?LEvel)
#define?_CONCRETE_AI_FACTORY_BODY_1(LAM,?AIType,?Level,?AIName)?LAM(AIType,?Level,?AIName)
#define?_CONCRETE_AI_FACTORY_BODY_2(LAM,?AIType,?Level,?AIName,?...)?LAM(AIType,?Level,?AIName)?_CONCRETE_AI_FACTORY_BODY_1(LAM,?AIType,?Level,?__VA_ARGS__)
#define?_CONCRETE_AI_FACTORY_BODY_3(LAM,?AIType,?Level,?AIName,?...)?LAM(AIType,?Level,?AIName)?_CONCRETE_AI_FACTORY_BODY_2(LAM,?AIType,?Level,?__VA_ARGS__)
#define?_CONCRETE_AI_FACTORY_BODY_4(LAM,?AIType,?Level,?AIName,?...)?LAM(AIType,?Level,?AIName)?_CONCRETE_AI_FACTORY_BODY_3(LAM,?AIType,?Level,?__VA_ARGS__)
#define?_CONCRETE_AI_FACTORY_BODY_5(LAM,?AIType,?Level,?AIName,?...)?LAM(AIType,?Level,?AIName)?_CONCRETE_AI_FACTORY_BODY_4(LAM,?AIType,?Level,?__VA_ARGS__)
#define?_GENERATE_AI_LAMBDA(AIType,?Level,?AIName)?[]()?requires?IsAbstractAI?{?return?new?AIName##Level;?},
#define?CONCRETE_AI_FACTORY(AIType,?Level,?...)?\
????_GET_OVERRIDE("ignored",?##__VA_ARGS__,?\
????_CONCRETE_AI_FACTORY_BODY_5,?_CONCRETE_AI_FACTORY_BODY_4,?\
????_CONCRETE_AI_FACTORY_BODY_3,?_CONCRETE_AI_FACTORY_BODY_2,?\
????_CONCRETE_AI_FACTORY_BODY_1,?_CONCRETE_AI_FACTORY_BODY_0)?\
????(_GENERATE_AI_LAMBDA,?AIType,?Level,?##__VA_ARGS__)
泛型宏的實(shí)現(xiàn)復(fù)雜是針對(duì)開(kāi)發(fā)者來(lái)說(shuō)的,對(duì)于使用者來(lái)說(shuō)卻是極為簡(jiǎn)單。
現(xiàn)在你可以非常簡(jiǎn)單地使用宏實(shí)現(xiàn)的「具體工廠」來(lái)替換前面的寫(xiě)法:
template<class?T>
static?constexpr?auto?AIFactory?=?AbstractAIFactory?{
????//?[]()?requires?IsAbstractAI?{?return?new?LuxEasy;?},
????//?[]()?requires?IsAbstractAI?{?return?new?ZiggsEasy;?},
????//?[]()?requires?IsAbstractAI?{?return?new?TeemoEasy;?}
????CONCRETE_AI_FACTORY(T,?Easy,?Lux,?Ziggs,?Teemo)
};
不論你有多少產(chǎn)品,都可以由該具體工廠輕松實(shí)現(xiàn),是不是很強(qiáng)大!
7
總結(jié)
本篇內(nèi)容應(yīng)該是我寫(xiě)的涉及內(nèi)容最廣的文章之一了,光之前寫(xiě)過(guò)的文章就引用了多篇。
所以對(duì)大家的要求也會(huì)有點(diǎn)高,可以多看幾遍。
另外這篇的內(nèi)容其實(shí)很“新”,首先組合使用了許多C++20特性,其次涉及了大量泛型編程技術(shù),文章介紹的泛型Lambda技術(shù)現(xiàn)在還不是很流行,使用場(chǎng)景也是慢慢摸索出來(lái)的,比較成熟的想法都寫(xiě)在了本文之中。
但是它的用處我感覺(jué)還有很多,還在研究中,后續(xù)再和大家分享。
