人們往往會將一個大問題拆解成許多小問題,通過解決一個個小問題,最終就能解決整個大問題。
若是拆解過后,這些小問題的處理邏輯不變,變化的只是輸入狀態(tài),那么此時就是一種代碼復(fù)用。對應(yīng)編程世界,一種是自上而下的拆解組合方式,稱為遞歸;一種是自下而上的拆解組合方式,稱為迭代。
拆解后還能組合,拆解才有意義,遞歸和迭代本身就帶有一種約束,必須具備起始狀態(tài)和終止?fàn)顟B(tài)。若是沒有起始狀態(tài),遞歸就沒有起點,循環(huán)就沒有開始;若是沒有終止?fàn)顟B(tài),遞歸就沒有終點,循環(huán)就沒有結(jié)束,
本文所說的元編程拆解技術(shù),就是編譯期的問題拆解與組合技術(shù),編譯期不像運行期那樣能夠動態(tài)地改變輸入與輸出狀態(tài),C++ 誕生了許多技術(shù)來解決這個問題,這便是后文要介紹的。
本文以一個需求為例,來講解這些技術(shù):
要求編寫一個 unroll
起始狀態(tài)就是 N,終止?fàn)顟B(tài)就是 N 為零,拆解后處理邏輯不變的小問題就是 callback。
先不看文,你首先想到的是什么解法?文讀畢,對比一下文中的各種拆解技術(shù)思路,獲益更多。
原始遞歸法
模板元編程最開始就只支持遞歸這一種拆解方式,每次輸入一個狀態(tài),依次產(chǎn)生下一個狀態(tài),若狀態(tài)與遞歸終止?fàn)顟B(tài)相同,則結(jié)束。
采用這種方式,實現(xiàn)需求如下:
?
1namespace?cpp98?{
2
3????//?declaration
4????template??void?unroll(F);
5
6????template?
7????struct?unroll_helper?{
8????????void?operator()(F?f)?{
9????????????f();
10????????????unroll(f);
11????????}
12????};
13
14????//?terminated?state
15????template?
16????struct?unroll_helper<0,?F>?{
17????????void?operator()(F)?{}
18????};
19
20????//?definition
21????template?
22????void?unroll(F?f)?{
23????????unroll_helper()(f);
24????}
25
26????void?print_cpp98()?{
27????????std::puts("hello?cpp98");
28????}
29}
30
31int?main()?{
32????cpp98::unroll<2>(cpp98::print_cpp98);
33????//?output:
34????//?hello?cpp98
35????//?hello?cpp98
36}
?
由于函數(shù)模板不支持偏特化,于是需要借助一個 unroll_helper 類模板,來實現(xiàn)遞歸終止條件,再在 unroll 函數(shù)中調(diào)用該幫助類,不斷拆解問題。
遞歸輸入條件 N 為起始狀態(tài),每次拆解執(zhí)行完成之后,通過 N - 1 得到下一個狀態(tài),從而不斷解決問題。
這個時期,C++ 的元編程拆解技術(shù)還很弱,一個簡單的需求,實現(xiàn)起來也頗為繁瑣。
可變參數(shù)模板
時間來到 C++11,模板元編程迎來強大擴展,非常有用的一個新特性就是可變參數(shù)模板,它能夠讓我們擺脫遞歸這種原始拆解方式。
但是 C++11 還只是開始,基礎(chǔ)組件不完善,所以并不能非常有效地實現(xiàn)目標(biāo)。
什么意思呢?看如下這個不太完善的實現(xiàn):
?
1namespace?cpp11?{
2
3????template?
4????class?index_sequence?{};
5
6????template?
7????void?unroll(F?f,?index_sequence)?{
8????????using?expand?=?std::size_t[];
9????????expand{?(f(),?Is)...?};
10????}
11
12}
13
14
15int?main()?{
16????cpp11::unroll([]?{?std::puts("hello?cpp11");?},
17????????cpp11::index_sequence<2,?1>());
18}
?
原始遞歸法是采用不斷遞歸來動態(tài)地產(chǎn)生狀態(tài),而有了可變參數(shù)模板,狀態(tài)可以直接在編譯期初期產(chǎn)生,從而直接拿來用就可以。
這里定義了一個 index_sequence 用來接收所有狀態(tài),然后借助一些逗號表達(dá)式技巧展開參數(shù)包,在參數(shù)包展開的過程當(dāng)中,執(zhí)行處理邏輯。
C++11 起也支持 Lambda,因此也不用再提供一個額外的調(diào)用函數(shù)。
這個實現(xiàn)的唯一缺點就是由于缺乏相應(yīng)的組件,需要手動產(chǎn)生狀態(tài),導(dǎo)致使用起來較為麻煩。
完善版可變參數(shù)模板
C++14 增加了 std::index_sequence 和 std::make_index_sequence,于是就能將手動產(chǎn)生狀態(tài)變成動態(tài)產(chǎn)生,完善實現(xiàn)。
代碼如下:
?
1namespace?cpp14?{
2
3????template?
4????void?helper(F?f,?std::index_sequence)?{
5????????using?expand?=?std::size_t[];
6????????expand{?(f(),?Is)...?};
7????}
8
9????//?variable?template
10????template?
11????auto?unroll?=?[](auto?f)?{?//?generic?lambda
12????????helper(f,?std::make_index_sequence{});
13????};
14}
15
16int?main()?{
17????cpp14::unroll<3>([]?{?std::puts("hello?cpp14");?});
18}
?
同時,C++14 還支持 variable template 和 generic lambda,這進一步簡化了實現(xiàn)。
Fold Expression
前面的方式是采用逗號表達(dá)式技巧來展開參數(shù)包,C++17 支持 Fold expression,可以直接展開,因此代碼得到進一步簡化。
變成:
?
1namespace?cpp17?{
2
3????template?
4????void?helper(F?f,?std::index_sequence)?{
5????????((f(),?Is),?...);?//?fold?expression
6????}
7
8????template?
9????auto?unroll?=?[](auto?f)?{?//?generic?lambda
10????????helper(f,?std::make_index_sequence{});
11????};
12}
?
constexpr if
C++17 的另一種拆解技術(shù)是借助 constexpr if,它的好處在于能夠直接在本函數(shù)內(nèi)判斷終止?fàn)顟B(tài),這樣就不再需要去定義一個遞歸終止函數(shù)。
?
1namespace?cpp17?{
2????//?variable?template?+?constexpr?if
3????template?
4????auto?unroll?=?[](auto?expr)?{
5????????if?constexpr?(N)?{
6????????????expr();
7????????????unroll(expr);
8????????}
9????};
10}
11
12int?main()?{
13????cpp17::unroll<3>([]?{?std::puts("hello?cpp17");?});
14}
?
與原始遞歸法相比,這種方式除了消除遞歸終止函數(shù),還免于編寫一個額外的 helper 類,generic lambda 更是減少了模板參數(shù)。
這是目前為止,最簡潔的實現(xiàn)。
C++20 雙層 Lambda 法
有沒有非遞歸的簡潔拆解方式呢?當(dāng)然也有。
看如下實現(xiàn):
?
1namespace?cpp20?{
2
3????template??constexpr?auto?unroll?=?[](auto?f)?{
4????????[f](std::index_sequence)?{
5????????????((f(),?void(Is)),?...);
6????????}(std::make_index_sequence());
7????};
8}
9
10int?main()?{
11????cpp20::unroll<3>([]?{?std::puts("hello?cpp20");?});
12}
?
這里的關(guān)鍵是 C++20 的 template lambda,它支持為 lambda 編寫模板參數(shù),基于此才能夠編寫索引的模板參數(shù)。
Lambda 函數(shù)里面再套一個 Lambda 函數(shù),外層用于提供調(diào)用接口,內(nèi)層用于管理狀態(tài)和處理調(diào)用。如果沒有 template lambda,內(nèi)層 Lambda 的 std::index_sequence 參數(shù)就無法寫,也就接收不了狀態(tài)。、
Structured Binding Packs
原本有些新特性是應(yīng)該在 C++23 就進入標(biāo)準(zhǔn)的,但由于種種原因,我們只有期望 C++26 能用上了。Structured binding packs 就是這么一個特性。
前面除了遞歸以外的所有拆解方法,都得借助 std::index_sequence,這就是代碼依舊復(fù)雜的原因所在。
有沒有一種方式可以直接讓我們訪問參數(shù)包,而不必再定義一個參數(shù)為 std::index_sequence 的函數(shù)才能拿到那些參數(shù)包?Structured binding packs 就提供了這一能力。
這是 P1061 所提出的一種方式,讓我們能夠通過 Structured bindings 直接得到參數(shù)包。
于是實現(xiàn)變?yōu)椋?/p>
?
1namespace?p1061?{
2
3????template??constexpr?auto?unroll?=?[](auto?f)?{
4????????auto?[...?Is]?=?std::make_index_sequence();
5????????((f(),?void(Is)),?...);
6????};
7
8}
9
10int?main()?{
11????p1061::unroll<3>([]?{?std::puts("hello?p1061");?});
12}
?
這種拆解技術(shù)才是最直觀的方式,兩行代碼解決一切。
Expansion Statements
另外一種方式就是我們在反射中經(jīng)常使用到的一個特性:template for。
這種方式比 Structured Binding Packs 更強大,是靜態(tài)反射里面的一個擴展特性,能夠支持編譯期迭代。
對于本次需求的實現(xiàn)為:
?
1namespace?p1306?{
2????template??constexpr?auto?unroll?=?[](auto?f)?{
3????????constexpr?std::array?dummy{};
4????????template?for?(auto&?e?:?dummy)
5????????????f();
6????};
7}
8
9int?main()?{
10????p1306::unroll<3>([]?{?std::puts("hello?p1306");?});
11}
?
這里借助了 std::array,構(gòu)建了一個并不會實際使用的變量,目的是為了當(dāng)作遍歷次數(shù)。
總結(jié)
本文從 C++98 開始介紹了許多拆解技術(shù),在不斷的優(yōu)化過程中,也能夠看到 C++ 的發(fā)展歷程。
由最原始的復(fù)雜、難用,到最后的兩行代碼搞定,也能夠看到 C++ 元編程的發(fā)展。
利用好這些技術(shù),對大家的元編程能力會有顯著提高。
審核編輯:湯梓紅
電子發(fā)燒友App


















評論