
寫模板代碼時(shí)常有這樣的需求手里有一組類型int, double, char, long想「給每個(gè)類型生成一個(gè)成員變量」「把每個(gè)類型變成它的指針」「只留下整型」…… 這些運(yùn)算發(fā)生在編譯期操作對(duì)象是「類型」而不是「值」。C 里表示「一組類型」的慣用法叫 typelist類型列表——它就是一個(gè)templateclass... struct typelist {}的空殼真正的功夫全在編譯期算法上。這些算法在boost::mp11、std::tuple的實(shí)現(xiàn)里到處都是讀懂它才能看懂那些庫。1. 引子為什么需要「類型列表」std::vectorint存的是「一堆 int 值」而 typelist 存的是「一堆類型」。它本身不占內(nèi)存、不產(chǎn)生任何對(duì)象純粹是給編譯器看的。最典型的應(yīng)用場(chǎng)景tuple 的迭代、variant 的類型集合、給每個(gè)類型生成一個(gè)字段的序列化框架。比如你想寫一個(gè)結(jié)構(gòu)體它的成員是int、double、string各一份模板代碼里就需要先「拿到這組類型」再逐個(gè)展開。typelist 就是那個(gè)「拿在手里的類型集合」。templatetypename...Tsstructtypelist{};// 空殼只「攜帶」類型不產(chǎn)生任何對(duì)象usingMyTypestypelistint,double,char,long;一個(gè) typelist 就是一個(gè)類型里面的Ts...是類型參數(shù)包parameter pack。要對(duì)它做運(yùn)算就要用**偏特化partial specialization**去「解包」它。官方文檔類型參數(shù)包與包展開parameter pack —— typelist 和所有變參模板的根基。2. 基礎(chǔ)算法length / at / append這三個(gè)是最基本的「列表操作」套路統(tǒng)一聲明主模板 → 偏特化匹配typelistTs...→ 用sizeof...(Ts)或遞歸解出結(jié)果。#includecstdio#includecstddef#includetype_traitstemplatetypename...Tsstructtypelist{};// length類型個(gè)數(shù)templatetypenameListstructlength;templatetypename...TsstructlengthtypelistTs...{staticconstexprstd::size_t valuesizeof...(Ts);};// at第 N 個(gè)類型遞歸N 每層減 1同時(shí)砍掉頭部templatestd::size_t N,typenameListstructat;templatetypenameHead,typename...Tailstructat0,typelistHead,Tail...{usingtypeHead;};// 遞歸基N0templatestd::size_t N,typenameHead,typename...TailstructatN,typelistHead,Tail...{usingtypetypenameatN-1,typelistTail...::type;};// append尾部追加一個(gè)類型templatetypenameList,typenameTstructappend;templatetypename...Ts,typenameTstructappendtypelistTs...,T{usingtypetypelistTs...,T;};intmain(){usingLtypelistint,double,char,long;static_assert(lengthL::value4,長度應(yīng)為 4);static_assert(std::is_same_vtypenameat0,L::type,int,第 0 個(gè)是 int);static_assert(std::is_same_vtypenameat2,L::type,char,第 2 個(gè)是 char);usingL2typenameappendL,float::type;static_assert(lengthL2::value5,追加后長度 5);static_assert(std::is_same_vtypenameat4,L2::type,float,第 4 個(gè)是 float);std::puts(所有 static_assert 通過length / at / append 正確);}所有 static_assert 通過length / at / append 正確要點(diǎn)atN, typelistHead, Tail...的遞歸是「每層丟掉 HeadN 減 1」直到N0時(shí)命中遞歸基返回當(dāng)前的 Head。這就是函數(shù)式編程里「頭尾分解」在模板里的翻版。官方文檔偏特化partial specialization —— 解包 typelist 必須靠它at的遞歸基at0, ...就是更特化的匹配。3. 核心map 與 filter有了頭尾分解就能寫「高階」編譯期算法map對(duì)每個(gè)類型套一個(gè)元函數(shù)F得到新列表。maptypelistint,char, add_pointer→typelistint*,char*。filter只保留滿足謂詞Pred的類型。filtertypelistint,char,double, is_integral→typelistint,char。#includecstdio#includecstddef#includetype_traitstemplatetypename...Tsstructtypelist{};// prepend頭部插入filter 回溯時(shí)用它把「保留」的 Head 拼回去templatetypenameList,typenameTstructprepend;templatetypename...Ts,typenameTstructprependtypelistTs...,T{usingtypetypelistT,Ts...;};// map對(duì)每個(gè)類型應(yīng)用元函數(shù) F取 FT::typetemplatetypenameList,templatetypenameclassFstructmap;templatetypename...Ts,templatetypenameclassFstructmaptypelistTs...,F{usingtypetypelisttypenameFTs::type...;};// filter遞歸保留滿足 Pred 的類型templatetypenameList,templatetypenameclassPredstructfilter;templatetemplatetypenameclassPredstructfiltertypelist,Pred{usingtypetypelist;};// 遞歸基空列表templatetypenameHead,typename...Tail,templatetypenameclassPredstructfiltertypelistHead,Tail...,Pred{usingresttypenamefiltertypelistTail...,Pred::type;// 先處理尾巴usingtypestd::conditional_tPredHead::value,typenameprependrest,Head::type,rest;};intmain(){usingLtypelistint,char,double,long,float;usingPtypenamemapL,std::add_pointer::type;static_assert(std::is_same_vP,typelistint*,char*,double*,long*,float*,map 后應(yīng)全是指針);usingItypenamefilterL,std::is_integral::type;static_assert(std::is_same_vI,typelistint,char,long,filter 后應(yīng)只剩整型);usingIPtypenamemapI,std::add_pointer::type;static_assert(std::is_same_vIP,typelistint*,char*,long*,map(filter) 組合整型指針列表);std::puts(所有 static_assert 通過map / filter 正確);}所有 static_assert 通過map / filter 正確map一步到位直接typelisttypename FTs::type...用包展開套用F。filter則必須遞歸先遞歸處理尾巴Tail...得到rest再根據(jù)PredHead::value決定要不要用prepend把Head插回頭部。遞歸的展開過程如下filter typelistint, char, double, std::is_integral 的遞歸展開 filterint, char, double ├─ Head int is_integralint true → 插回頭部 └─ filterchar, double ├─ Head char is_integralchar true → 插回頭部 └─ filterdouble ├─ Head double is_integraldouble false → 丟棄 └─ filter 遞歸基返回 typelist 回溯typelist → typelistchar → typelistint, char 最終結(jié)果注意回溯的順序因?yàn)槊恳徊蕉加胮repend往頭部插所以結(jié)果保持了原列表的相對(duì)順序。這是 filter 實(shí)現(xiàn)里最容易寫反的地方用append會(huì)得到反序列表。官方文檔std::conditional編譯期 if —— filter 里「保留還是丟棄」靠它做編譯期分支。4. typelist 與 std::tuple 的關(guān)系兩者長得像但分工完全不同維度typeliststd::tuple存的是什么純類型沒有值類型對(duì)應(yīng)的實(shí)例值運(yùn)行時(shí)機(jī)純編譯期零運(yùn)行時(shí)開銷運(yùn)行期對(duì)象能否 sizeof否不是對(duì)象是占內(nèi)存典型用途類型運(yùn)算map / filter / at存異構(gòu)數(shù)據(jù)、多返回值、參數(shù)打包轉(zhuǎn)換to_tupletypelistTs...::type std::tupleTs...std::tuple_element_t取回類型簡(jiǎn)單說typelist 是「類型的清單」tuple 是「把清單落地成值」。很多庫比如boost::mp11就是先用 typelist 算好類型再轉(zhuǎn)成 tuple 或結(jié)構(gòu)體去用。5. 落地把 typelist 變成能用的東西typelist 本身不能跑最終要「落地」。下面是兩個(gè)典型落地方式轉(zhuǎn)成std::tuple以及在編譯期對(duì)類型做聚合計(jì)算這里用折疊表達(dá)式求所有類型sizeof之和。#includecstdio#includecstddef#includetuple#includetype_traitstemplatetypename...Tsstructtypelist{};// typelist - std::tupletemplatetypenameListstructto_tuple;templatetypename...Tsstructto_tupletypelistTs...{usingtypestd::tupleTs...;};// 編譯期聚合所有類型的 sizeof 之和C17 折疊表達(dá)式templatetypenameListstructtotal_size;templatetypename...Tsstructtotal_sizetypelistTs...{staticconstexprstd::size_t value(sizeof(Ts)...std::size_t{0});};intmain(){usingLtypelistint,double,char,long;usingTtypenameto_tupleL::type;static_assert(std::is_same_vT,std::tupleint,double,char,long,typelist 應(yīng)能落地成 tuple);T t{42,3.14,x,99L};std::printf(tuple 元素個(gè)數(shù): %zu\n,std::tuple_size_vT);std::printf(第 0 個(gè)元素: %d\n,std::get0(t));std::printf(第 1 個(gè)元素: %.2f\n,std::get1(t));std::printf(所有類型 sizeof 之和: %zu\n,total_sizeL::value);}tuple 元素個(gè)數(shù): 4 第 0 個(gè)元素: 42 第 1 個(gè)元素: 3.14 所有類型 sizeof 之和: 21total_size里的(sizeof(Ts) ... std::size_t{0})是 C17 的折疊表達(dá)式fold expression把「對(duì)每個(gè)類型算 sizeof 再累加」這件事一行寫完——這也是 typelist 類算法在現(xiàn)代 C 里被逐步替代的信號(hào)。官方文檔std::tuple — cppreference —— typelist 最常見的落地目標(biāo)。6. 現(xiàn)狀Concepts 與折疊表達(dá)式取代了什么手寫 typelist 遞歸 map/filter 是 C11 時(shí)代的主流但現(xiàn)在有了更順手的工具很多場(chǎng)景不必再手寫需求老辦法typelist 遞歸新辦法對(duì)每個(gè)類型做一件事遞歸 偏特化折疊表達(dá)式(f(Ts), ...)編譯期 ifstd::conditional/ tag dispatchif constexprC17約束模板參數(shù)SFINAE / enable_ifConceptsC20類型集合運(yùn)算map/filter/at手寫 typelistboost::mp11不過有兩個(gè)理由仍然要讀懂typelist第一boost::mp11、std::variant、std::tuple這些庫內(nèi)部就是這套東西讀源碼繞不開第二std::tuple_element_tN, Tuple、std::variant的類型運(yùn)算本質(zhì)就是 typelist 的at。理解了底層的「頭尾分解 遞歸 偏特化」再看到mp_list那種高級(jí)寫法就不會(huì)發(fā)怵。7. 延伸閱讀類型參數(shù)包parameter pack— cppreferencetypelist 與包展開的地基偏特化partial specialization— cppreferenceat/filter的遞歸基就是靠它「更特化優(yōu)先」匹配std::conditional — cppreference編譯期分支的核心工具std::tuple — cppreferencetypelist 最常見的落地目標(biāo)配合std::tuple_element就是內(nèi)置的at本知識(shí)庫內(nèi)的相關(guān)篇目《tag dispatch 與重載偏序?qū)崙?zhàn)按迭代器類別派發(fā)算法》 —— tag dispatch標(biāo)簽派發(fā)用「空類型」當(dāng)標(biāo)簽《表達(dá)式模板Eigen 式延遲求值把 abc 壓成一次遍歷》 —— 表達(dá)式模板Expression Templates讓 abc 不再生成中間臨時(shí)對(duì)象《編譯期反射模擬C 沒有反射怎么辦》 —— C 至今沒有運(yùn)行時(shí)反射想「遍歷結(jié)構(gòu)體成員」「自動(dòng)序列化」只能靠模擬8. 一句話總結(jié)typelist 用一個(gè)空殼templateclass... struct typelist攜帶一組類型length/at/map/filter 全靠「頭尾分解 遞歸 偏特化」實(shí)現(xiàn)結(jié)果用static_assert在編譯期驗(yàn)證現(xiàn)代 C 里它正被 Concepts、折疊表達(dá)式、if constexpr部分取代但讀boost::mp11、std::tuple的源碼這套手藝仍然是繞不開的底子。