1. 程式人生 > 實用技巧 >從 C++98 到 C++17,超程式設計是如何演進的? | 技術頭條

從 C++98 到 C++17,超程式設計是如何演進的? | 技術頭條

從 C++98 到 C++17,超程式設計是如何演進的? | 技術頭條

作者 | 祁宇
責編 | 郭芮
出品 | CSDN(ID:CSDNnews)

不斷出現的C++新的標準,正在改變超程式設計的程式設計思想,新的idea和方法不斷湧現,讓超程式設計變得越來越簡單,讓C++變得簡單也是C++未來的一個趨勢。

很多人對超程式設計有一些誤解,認為程式碼晦澀難懂,編譯錯誤提示很糟糕,還會讓編譯時間變長,對超程式設計有一種厭惡感。不可否認,超程式設計確實有這樣或那樣的缺點,但是它同時也有非常鮮明的優點:

  • zero-overhead的編譯期計算;

  • 簡潔而優雅地解決問題;

  • 終極抽象。

在我看來超程式設計最大的魅力是它常常能化腐朽為神奇,幫我們寫出dream code!

C++98模版超程式設計思想

C++98中的模版超程式設計通常和這些特性和方法有關:

  • 元函式;

  • SFINAE;

  • 模版遞迴;

  • 遞迴繼承;

  • Tag Dispatch;

  • 模版特化/偏特化。

元函式

元函式就是編譯期函式呼叫的類或模版類。比如下面這個例子:

template<classT>
structadd_pointer{typedefT*type;};

typedeftypenameadd_pointer<int>::typeint_pointer;

addpointer就是一個元函式(模版類),元函式的呼叫是通過訪問其sub-type實現的,比如addpointer::type就是呼叫add_pointer元函數了。

這裡面型別T作為元函式的value,型別是超程式設計中的一等公民。模版超程式設計概念上是函數語言程式設計,對應於一個普通函式,值作為引數傳給函式,在模版元裡,型別作為元函式的引數被傳來傳去。

SFINAE

替換失敗不是錯誤。

template<boolB,classT=void>
structenable_if{};

template<classT>
structenable_if<true,T>{typedefTtype;};

template<classT>
typenameenable_if<sizeof(T)==4,void>::type
foo(Tt){}

foo(1);//ok
foo('a');//compileerror

在上面的例子中,呼叫foo('a')模版函式的時候,有一個模版例項化的過程,這個過程中會替換模版引數,如果模版引數替換失敗,比如不符合編譯期的某個條件,那麼這個模版例項化會失敗,但是這時候編譯器不認為這是一個錯誤,還會繼續尋找其他的替換方案,直到所有的都失敗時才會產生編譯錯誤,這就是SFINAE。SFINAE實際上是一種編譯期的選擇,不斷去選擇直到選擇到一個合適的版本位置,其實它也可以認為是基於模板例項化的tag dispatch。

模版遞迴,模版特化

template<intn>structfact98{
staticconstintvalue=n*fact98<n-1>::value;
};
template<>structfact98<0>{
staticconstintvalue=1;
};
std::cout<<fact98<5>::value<<std::endl;

這是模版超程式設計的hello world例子,通過模版特化和模版遞迴實現編譯期計算。

在C++98中模版超程式設計的集大成者的庫是boost.mpl和boost.fusion,boost.mpl主要提供了編譯期型別容器和演算法,boost.fusion通過異構的編譯期容器融合編譯期和執行期計算。

structprint{
template<typenameT>
voidoperator()(Tconst&x)const{
std::cout<<typeid(x).name()<<std::endl;
}
};

template<typenameSequence>
voidprint_names(Sequenceconst&seq){
for_each(filter_if<boost::is_class<_>>(seq),print());
}
boost::fusion::vector<int,char,std::string>stuff(2018,'i',"purecpp");
print_names(stuff);

上面這個例子遍歷boost::fusion::vector異構容器,列印其中的string型別。

關於C++98模版元的書可以看《modern c++ design》和《c++ templates》。

Modern C++ metaprogramming程式設計思想

C++11中的超程式設計思想

Modern C++新標準對於超程式設計有著深刻的影響,一些新的程式設計思想和方法湧現,但總體趨勢是超程式設計變得更簡單了。比如C++98中的add_pointer元函式,我們需要寫一個模版類:

template<classT>
structadd_pointer{typedefT*type;};

typedeftypenameadd_pointer<int>::typeint_pointer;

而在C++11中我們只需要使用C++11的新特性模版別名就可以定義一個add_pointer元函數了,程式碼變得更簡潔了。

template<classT>usingadd_pointer=T*;

usingint_pointer=add_pointer<int>;

在C++11中,元函式由模版類變為模版別名了。C++11中提供了大量元函式在type_traits庫中,這樣我們不用再自己寫了,直接拿過來使用就行了。

C++11中另外的一個新特性variadic template可以作為一個型別容器,我們可以通過variadic templates pack訪問模版引數,不需要通過模版遞迴和特化來訪問模版引數。

template<typename...Values>structmeta_list{};
usinglist_of_ints=meta_list<char,short,int,long>;

template<classList>structlist_size;
template<template<class...>classList,class...Elements>
structlist_size<List<Elements...>>
:std::integral_constant<std::size_t,sizeof...(Elements)>{};

constexprautosize=list_size<std::tuple<int,float,void>>::value;
constexprautosize1=list_size<list_of_ints>::value;
constexprautosize2=list_size<boost::variant<int,float>>::value;

通過variadic template pack讓編譯器幫助我們訪問型別,比C++98中通過模版遞迴和特化來訪問型別效率更高。

C++11中另外一個新特性constexpr也讓我們編寫元函式變得更簡單了。

在C++98中:

template<intn>structfact98{
staticconstintvalue=n*fact98<n-1>::value;
};
template<>structfact98<0>{
staticconstintvalue=1;
};
std::cout<<fact98<5>::value<<std::endl;

在C++11中:

constexprintfact11(intn){
returnn<=1?1:(n*fact11(n-1));
}

我們不再需要通過模版特化和遞迴來做編譯期計算了,我們直接通過新的關鍵字constexpr來實現編譯期計算,它修飾一個函式,表明這個函式是在編譯期計算的,這個函式和一個普通函式看起來幾乎沒有分別,唯一的差別就是多了一個constexpr,比C++98的寫法簡單多了。

不過在C++11中constexpr的限制比較多,比如說constexpr函式中只能是個表示式,無法使用變數,迴圈等語句,在C++14中就去掉這個限制了,讓我們可以更方便地寫編譯期計算的函數了。

C++14中的超程式設計思想

//inc++11
constexprintfact11(intn){
returnn<=1?1:(n*fact11(n-1));
}

//inc++14
constexprintfact14(intn){
ints=1;
for(inti=1;i<=n;i++){s=s*i;}
returns;
}

可以看到在C++14中我們寫constexpr編譯期計算的函式時,不必受限於表示式語句了,可以定義變數和寫迴圈語句了,這樣也不用通過遞迴去計算了,直接通過迴圈語句就可以得到編譯期計算結果了,使用起來更方便了。

在C++14中除了constexpr增強之外,更重要的幾個影響超程式設計思想的特性是constexpr, generic lambda, variable template。新標準、新特性會產生新的程式設計思想,在C++14裡超程式設計的程式設計思想發生了重大的變化!

在2014年Louis Dionne用C++14寫的一個叫Hana的超程式設計庫橫空出世,它的出現在C++社群引起震動,因為它所採用的方法不再是經典的模版元的那一套方法了,是真正意義上的函數語言程式設計實現的。模版元在概念上是函數語言程式設計,而Hana是第一次在寫法上也變成函數語言程式設計了,這是C++超程式設計思想的一個重大改變。

Boost.Hana的程式設計思想

通過一個例子來看Boost.Hana的程式設計思想:

template<typenameT>
structtype_wrapper{
usingtype=T;
};

template<typenameT>
type_wrapper<T>type{};

//typetovalue
autothe_int_type=type<int>;

//valuetotype
usingthe_real_int_type=decltype(the_int_type)::type;

這裡我們定義了一個型別的wraper,裡面只有一個子型別,接著定義這個wraper的變數模版,有了這個變數模版,我們就可以很方便的實現type-to-value和value-to-type了。

某個具體型別的變數模版就代表一個值,通過decltype這個值就能得到變數模版的型別了,有了這個變數模版,我們就可以通過Lambda寫元函數了,這裡的Lambda是C++14中的generic lambda,這個Lambda的引數就是一個變數模版值,在Lambda表示式中,我們可以對獲取值的sub type並做轉換,然後再返回變換之後的變數模版值。

template<typenameT>
type_wrapper<T>type{};

constexprautoadd_pointer=[](autot){
usingT=typenamedecltype(t)::type;
returntype<std::add_pointer_t<T>>;//typetovalue
};

constexprautointptr=add_pointer(type<int>);
static_assert(std::is_same_v<decltype(intptr)::type,int*>);//valuetotype

這裡的add_pointer元函式不再是一個模版類或者模版別名了,而是一個Lambda表示式。這裡面關鍵的兩個地方是如何把型別變為值和把值變為型別,通過C++14的變數模版就可以實現這個目標了。

Boost.Hana的目標是通過型別容器融合編譯期和執行期計算,替代boost.mpl和boost.fusion!比如下面的例子:

autoanimal_types=hana::make_tuple(hana::type_c<Fish*>,hana::type_c<Cat&>,hana::type_c<Dog*>);
autoanimal_ptrs=hana::filter(animal_types,[](autoa){
returnhana::traits::is_pointer(a);
});

static_assert(animal_ptrs==hana::make_tuple(hana::type_c<Fish*>,hana::type_c<Dog*>),"");

autoanimals=hana::make_tuple(Fish{"Nemo"},Cat{"Garfield"},Dog{"Snoopy"});
autonames=hana::transform(animals,[](autoa){
returna.name;
});
assert(hana::reverse(names)==hana::make_tuple("Snoopy","Garfield","Nemo"));

我們既可以操作型別容器中的型別,又可以操作型別容器中的執行期的值,Hana可以幫我們很方便地融合編譯期與執行期的計算。

Boost.Hana的特點:

  • 元函式不再是類或類模版,而是lambda;

  • 不再基於型別,而是基於值;

  • 沒有SFINAE,沒有模版遞迴;

  • 函數語言程式設計;

  • 程式碼更容易理解;

  • 超程式設計變得更簡單;

  • 融合編譯期與執行期。

以Boost.Hana為代表的超程式設計實現不再是經典的type level的思想了,而是以C++14新特性實現的lambda level的函數語言程式設計思想了。

C++17超程式設計思想

在C++17中,超程式設計得到了進一步地簡化,比如我們之前需要藉助模版特化,SFINAE才能實現的編譯期選擇,現在通過if constexpr就可以很輕鬆的實現了。

在C++98中:

template<std::size_tI>
auto&get(person&p);

template<>
auto&get<0>(person&p){
returnp.id;
}

template<>
auto&get<1>(person&p){
returnp.name;
}

template<>
auto&get<2>(person&p){
returnp.age;
}

在C++17中:

template<std::size_tI>
auto&get(person&p){
ifconstexpr(I==0){
returnp.id;
}
elseifconstexpr(I==1){
returnp.name;
}
elseifconstexpr(I==2){
returnp.age;
}
}

這裡不再需要模版特化了,也不需要拆分成多個函數了,就像普通的if-else語句一樣寫編譯期選擇的程式碼,簡潔易懂!

在C++14中:

template<typenameT>
std::enable_if_t<std::is_same_v<T,std::string>,std::string>to_string(Tt){
returnt;
}

template<typenameT>
std::enable_if_t<!std::is_same_v<T,std::string>,std::string>to_string(Tt){
returnstd::to_string(t);
}

在C++17中:

template<typenameT>
std::stringto_string(Tt){
ifconstexpr(std::is_same_v<T,std::string>)
returnt;
else
returnstd::to_string(t);
}

這裡不再需要SFINAE了,同樣可以實現編譯期選擇,程式碼更加簡潔。

C++超程式設計的庫以這些庫為代表,這些庫代表了C++超程式設計思想不斷演進的一個趨勢:

  • C++98:boost.mpl,boost.fusion

  • C++11:boost.mp11,meta,brigand

  • C++14:boost.hana

從C++98到Modern C++,C++新標準新特性產生新的idea,讓超程式設計變得更簡單更強大,Newer is Better!

Modern C++超程式設計應用

編譯期檢查

超程式設計的一個典型應用就是編譯期檢查,這也是超程式設計最簡單的一個應用,簡單到用一行程式碼就可以實現編譯期檢查。比如我們需要檢查程式執行的系統是32位的還是64位的,通過一個簡單的assert就可以實現了。

static_assert(sizeof(void*)==8,"expected64-bitplatform");

當系統為32位時就會產生一個編譯期錯誤並且編譯器會告訴你錯誤的原因。

這種編譯期檢查比通過#if define巨集定義來檢查系統是32位還是64位好得多,因為巨集定義可能存在忘記寫的問題,並不能在編譯期就檢查到錯誤,要到執行期才能發現問題,這時候就太晚了。

再看一個例子:

template<typenameT,intRow,intColumn>
structMatrix{
static_assert(Row>=0,"Rownumbermustbepositive.");
static_assert(Column>=0,"Columnnumbermustbepositive.");
static_assert(Row+Column>0,"RowandColumnmustbegreaterthan0.");
};

在這個例子中,這個Matrix是非常安全的,完全不用擔心定義Matrix時行和列的值寫錯了,因為編譯器會在編譯期提醒你哪裡寫錯了,而不是等到執行期才發現錯誤。

除了經常用staticassert做編譯期檢查之外,我們還可以使用enableif來做編譯期檢查。

structA{
voidfoo(){}
intmember;
};

template<typenameFunction>
std::enable_if_t<!std::is_member_function_pointer_v<Function>>foo(Function&&f){
}

foo([]{});//ok
foo(&A::foo);//compileerror:nomatchingfunctionforcallto'foo(void(A::*)())'

比如這個程式碼,我們通過std::enableift來限定輸入引數的型別必須為非成員函式,如果傳入了成員函式則會出現一個編譯期錯誤。

超程式設計可以讓我們的程式碼更安全,幫助我們儘可能早地、在程式執行之前的編譯期就發現bug,讓編譯器而不是人來幫助我們發現bug。

編譯期探測

超程式設計可以幫助我們在編譯期探測一個成員函式或者成員變數是否存在。

template<class,class=void>
structhas_foo:std::false_type{};

template<classT>
structhas_foo<T,std::void_t<decltype(std::declval<T>().foo())>>:
std::true_type{};

template<class,class=void>
structhas_member:std::false_type{};

template<classT>
structhas_member<T,std::void_t<decltype(std::declval<T>().member)>>:
std::true_type{};

structA{
voidfoo(){}
intmember;
};
static_assert(has_foo<A>::value);
static_assert(has_member<A>::value);

我們藉助C++17的void_t,就可以輕鬆實現編譯期探測功能了,這裡實際上是利用了SFINAE特性,當decltype(std::declval().foo())成功了就表明存在foo成員函式,否則就不存在。

通過編譯期探測我們可以很容易實現一個AOP(Aspect Oriented Programming)功能,AOP可以通過一系列的切面幫我們把核心邏輯和非核心邏輯分離。

server.set_http_handler<GET,POST>("/aspect",[](request&req,response&res){
res.render_string("helloworld");
},check{},log_t{});

上面這段程式碼的核心邏輯就是返回一個hello world,非核心邏輯就是檢查輸入引數和記錄日誌,把非核心邏輯分離出來放到兩個切面中,不僅僅可以讓我們的核心邏輯保持簡潔,還可以讓我們可以更專注於核心邏輯。

實現AOP的思路很簡單,通過編譯期探測,探測切面中是否存在before或者after成員函式,存在就呼叫。

constexprboolhas_befor_mtd=has_before<decltype(item),request&,response&>::value;
ifconstexpr(has_befor_mtd)
r=item.before(req,res);

constexprboolhas_after_mtd=has_after<decltype(item),request&,response&>::value;
ifconstexpr(has_after_mtd)
r=item.after(req,res);

為了讓編譯期探測的程式碼能複用,並且支援可變模版引數,我們可以寫一個通用的編譯期探測的程式碼:

#defineHAS_MEMBER(member)\
template<typenameT,typename...Args>\
structhas_##member\
{\
private:\
template<typenameU>staticautoCheck(int)->decltype(std::declval<U>().member(std::declval<Args>()...),std::true_type());\
template<typenameU>staticstd::false_typeCheck(...);\
public:\
enum{value=std::is_same<decltype(Check<T>(0)),std::true_type>::value};\
};

HAS_MEMBER(before)
HAS_MEMBER(after)

具體程式碼可以參考這裡:https://github.com/qicosmos/feather。

注:這段巨集程式碼可以用c++20的std::is_detected替代,也可以寫一個C++14/17的程式碼來替代這個巨集:

namespace{
structnonesuch{
nonesuch()=delete;
~nonesuch()=delete;
nonesuch(constnonesuch&)=delete;
voidoperator=(constnonesuch&)=delete;
};

template<classDefault,classAlwaysVoid,
template<class...>classOp,class...Args>
structdetector{
usingvalue_t=std::false_type;
usingtype=Default;
};


template<classDefault,template<class...>classOp,class...Args>
structdetector<Default,std::void_t<Op<Args...>>,Op,Args...>{
usingvalue_t=std::true_type;
usingtype=Op<Args...>;
};

template<template<class...>classOp,class...Args>
usingis_detected=typenamedetector<nonesuch,void,Op,Args...>::value_t;

template<template<class...>classOp,class...Args>
usingdetected_t=typenamedetector<nonesuch,void,Op,Args...>::type;

template<classT,typename...Args>
usinghas_before_t=decltype(std::declval<T>().before(std::declval<Args>()...));

template<classT,typename...Args>
usinghas_after_t=decltype(std::declval<T>().after(std::declval<Args>()...));
}

template<typenameT,typename...Args>
usinghas_before=is_detected<has_before_t,T,Args...>;

template<typenameT,typename...Args>
usinghas_after=is_detected<has_after_t,T,Args...>;

編譯期計算

編譯期計算包含了較多內容,限於篇幅,我們重點說一下型別萃取的應用:

  • 型別計算;

  • 型別推導;

  • 型別萃取;

  • 型別轉換;

  • 數值計算:表示式模版,Xtensor,Eigen,Mshadow。

我們可以通過一個function_traits來萃取可呼叫物件的型別、引數型別、引數個數等型別資訊。

template<typenameRet,typename...Args>
structfunction_traits_impl<Ret(Args...)>{
public:
enum{arity=sizeof...(Args)};
typedefRetfunction_type(Args...);
typedefRetresult_type;
usingstl_function_type=std::function<function_type>;
typedefRet(*pointer)(Args...);

template<size_tI>
structargs{
static_assert(I<arity,"indexisoutofrange,indexmustlessthansizeofArgs");
usingtype=typenamestd::tuple_element<I,std::tuple<Args...>>::type;
};

typedefstd::tuple<std::remove_cv_t<std::remove_reference_t<Args>>...>tuple_type;
usingargs_type_t=std::tuple<Args...>;
};

完整程式碼可以參考這裡:https://github.com/qicosmos/cinatra。

有了這個function_traits之後就方便實現一個RPC路由了,以rest_rpc為例(https://github.com/qicosmos/rest_rpc):

structrpc_service{
intadd(inta,intb){returna+b;}

std::stringtranslate(conststd::string&orignal){
std::stringtemp=orignal;
for(auto&c:temp)c=toupper(c);
returntemp;
}
};


rpc_serverserver;
server.register_handler("add",&rpc_service::add,&rpc_srv);
server.register_handler("translate",&rpc_service::translate,&rpc_srv);

autoresult=client.call<int>("add",1,2);
autoresult=client.call<std::string>("translate","hello");

RPCServer註冊了兩個服務函式add和translate,客戶端發起RPC呼叫,會傳RPC函式的實際引數,這裡需要把網路傳過來的位元組對映到一個函式並呼叫,這裡就需要一個RPC路由來做這個事情。下面是RestRPC路由的實現:

template<typenameFunction>
voidregister_nonmember_func(std::stringconst&name,constFunction&f){
this->map_invokers_[name]={std::bind(&invoker<Function>::apply,f,std::placeholders::_1,std::placeholders::_2,std::placeholders::_3)};
}

template<typenameFunction>
structinvoker{
staticvoidapply(constFunction&func,constchar*data,size_tsize,
std::string&result){
usingargs_tuple=typenamefunction_traits<Function>::args_tuple;
msgpack_codeccodec;
autotp=codec.unpack<args_tuple>(data,size);
call(func,result,tp);
}
};

RPCServer註冊RPC服務函式的時候,函式型別會儲存在invoker中,後面收到網路位元組的時候,我們通過functiontraits萃取出函式引數對應的tuple型別,反序列化得到一個例項化的tuple之後就可以藉助C++17的std::apply實現函式呼叫了。詳細程式碼可以參考rest_rpc。

編譯期反射

通過編譯期反射,我們可以得到型別的元資料,有了這個元資料之後我們就可以用它做很多有趣的事情了。可以用編譯期反射實現:

  • 序列化引擎;

  • ORM;

  • 協議介面卡。

以序列化引擎iguana(https://github.com/qicosmos/iguana)來舉例,通過編譯期反射可以很容易的將元資料對映為json、xml、msgpack或其他格式的資料。

structperson{
std::stringname;
intage;
};
REFLECTION(person,name,age)

personp={"tom",20};
iguana::string_streamss;

to_xml(ss,p);
to_json(ss,p);
to_msgpack(ss,p);
to_protobuf(ss,p);

以ORM引擎(https://github.com/qicosmos/ormpp)舉例,通過編譯期反射得到的元資料可以用來自動生成目標資料庫的SQL語句:

ormpp::dbng<mysql>mysql;
ormpp::dbng<sqlite>sqlite;
ormpp::dbng<postgresql>postgres;

mysql.create_datatable<person>();
sqlite.create_datatable<person>();
postgres.create_datatable<person>();

反射將進入C++23標準,未來的C++標準中的反射將更強大和易用。

融合編譯期和執行期

執行期和編譯期存在一個巨大的鴻溝,而在實際應用中我需要融合編譯期與執行期,這時候就需要一個橋樑來連線編譯期與執行期。編譯期和執行期從概念上可以簡單地認為分別代表了type和value,融合的關鍵就是如何實現type to value以及value to type。

Modern C++已經給我們提供了便利,比如下面這個例子:

autoval=std::integral_constant<int,5>{};
usingint_type=decltype(val);

autov=decltype(val)::value;

我們可以很方便地將一個值變為一個型別,然後由通過型別獲得一個值。接下來我們來看一個具體的例子:如何根據一個執行時的值呼叫一個編譯期模版函式?

template<size_tN>
voidfun(){}

voidfoo(intn){
switch(n){
case0:
fun<0>();
break;
case1:
fun<1>();
break;
case2:
fun<2>();
break;
default:
break;
}
}

這個程式碼似乎很好地解決了這個問題,可以實現從執行期數值到編譯期模版函式呼叫。但是如果這個執行期數值越來越大的時候,我們這個switch就會越來越長,還存在寫錯的可能,比如呼叫了foo(100),那這時候真的需要寫100個switch-case嗎?所以這個寫法並不完美。

我們可以藉助tuple來比較完美地解決這個問題:

namespacedetail{
template<classTuple,classF,std::size_t...Is>
voidtuple_switch(conststd::size_ti,Tuple&&t,F&&f,std::index_sequence<Is...>){
(void)std::initializer_list<int>{
(i==Is&&(
(void)std::forward<F>(f)(std::integral_constant<size_t,Is>{}),0))...
};
}
}//namespacedetail

template<classTuple,classF>
inlinevoidtuple_switch(conststd::size_ti,Tuple&&t,F&&f){
constexprautoN=
std::tuple_size<std::remove_reference_t<Tuple>>::value;

detail::tuple_switch(i,std::forward<Tuple>(t),std::forward<F>(f),
std::make_index_sequence<N>{});
}

voidfoo(intn){
std::tuple<int,int,int>tp;
tuple_switch(n,tp,[](autoitem){
constexprautoI=decltype(item)::value;
fun<I>();
});
}

foo(1);
foo(2);

通過一個tuple_switch就可以通過執行期的值呼叫編譯期模版函數了,不用switch-case了。關於之前需要寫很長的switch-case語句的問題,也可以藉助超程式設計來解決:

template<size_t...Is>
automake_tuple_from_sequence(std::index_sequence<Is...>)->decltype(std::make_tuple(Is...)){
std::make_tuple(Is...);
}

template<size_tN>
constexprautomake_tuple_from_sequence()->decltype(make_tuple_from_sequence(std::make_index_sequence<N>{})){
returnmake_tuple_from_sequence(std::make_index_sequence<N>{});
}

voidfoo(intn){
decltype(make_tuple_from_sequence<100>())tp;//std::tuple<int,int,…,int>
tuple_switch(n,tp,[](autoitem){
constexprautoI=decltype(item)::value;
fun<I>();
});
}

foo(98);
foo(99);

這裡的decltype(maketuplefrom_sequence<100>())會自動生成一個有100個int的tuple<int,...,int>輔助型別,有了這個輔助型別,我們完全不必要去寫長長的switch-case語句了。

有人也許會擔心,這裡這麼長的tuple<int,...,int>會不會生成100個Lambda例項化程式碼?這裡其實不用擔心,因為編譯器可以做優化,優化的情況下只會生成一次Lambda例項化的程式碼,而且實際場景中不可能存在100個分支的程式碼。

介面的泛化與統一

超程式設計可以幫助我們融合底層異構的子系統、遮蔽介面或系統的差異、提供統一的介面。

以ORM為例:

MySQL connect

mysql_real_connect(handle,"127.0.0.1","feather","2018","testdb",0,nullptr,0);

PostgreSQL connect

PQconnectdb("host=localhostuser=127.0.0.1password=2018dbname=testdb");

Sqlite connect

sqlite3_open("testdb",handle);

ORM unified connect interface

ORM::mysql.connect("127.0.0.1",“feather",“2018","testdb");
ORM::postgres.connect("127.0.0.1",“feather",“2018","testdb");
ORM::sqlite.connect("testdb");

不同的資料庫的C connector相同功能的介面是完全不同的,ormpp庫(https://github.com/qicosmos/ormpp)要做的一件事就是要遮蔽這些介面的差異,讓使用者可以試用統一的介面來操作資料庫,完全感受不到底層資料庫的差異。

超程式設計可以幫助我們實現這個目標,具體思路是通過可變引數模版來統一介面,通過policy-base設計和variadic templates來遮蔽資料庫介面差異。

template<typenameDB>
classdbng{
template<typename...Args>
boolconnect(Args&&...args){
returndb_.connect(std::forward<Args>(args)...);
}

template<typename...Args>
boolconnect(Args...args){
ifconstexpr(sizeof...(Args)==5){
returnstd::apply(&mysql_real_connect,std::make_tuple(args...);
}
elseifconstexpr(sizeof...(Args)==4){//postgresql}
elseifconstexpr(sizeof...(Args)==2){//sqlite}
}

這裡通過connect(Args... args)統一連線資料庫的介面,然後再connect內部通過if constexpr和變參來選擇不同的分支。if constexpr加variadic templates等於靜態多型,這是C++17給我們提供的一種新的實現靜態多型方法。

這樣的好處是可以通過增加引數或修改引數型別方式來擴充套件介面,沒有繼承,沒有SFINAE,沒有模版特化,簡單直接。

消除重複(巨集)

很多人喜歡用巨集來減少手寫重複的程式碼,比如下面這個例子,如果對每個列舉型別都寫一個寫到輸出流裡的程式碼段,是重複而繁瑣的,於是就通過一個巨集來消除這些重複程式碼(事實上,這些重複程式碼仍然會生成,只不過由編譯器幫助生成了)。

#defineENUM_TO_OSTREAM_FUNC(EnumType)\
std::ostream&operator<<(std::ostream&out_stream,constEnumType&x){\
out_stream<<static_cast<int>(x);\
returnout_stream;\
}

enumclassMsgType{Connected,Timeout};
enumclassDataType{Float,Int32};
ENUM_TO_OSTREAM_FUNC(MsgType);
ENUM_TO_OSTREAM_FUNC(DataType);

這看似是使用巨集的合適場景,但是巨集最大的問題是程式碼無法除錯,程式碼的易讀性差,但是用超程式設計,我們不用寫這個巨集了,也不用去寫巨集定義了。

template<typenameT,typename=
typenamestd::enable_if<std::is_enum<T>::value>::type>
std::ostream&operator<<(std::ostream&out_stream,Tx){
out_stream<<static_cast<int>(x);
returnout_stream;
}

超程式設計比巨集更好地解決了問題。

再看一個巨集的例子:

#defineCALL(name,...)\
do{\
resultret=func(name);\
if(ret==0){\
__VA_ARGS__;\
do_something(name);\
}\
else{\
do_something(name);\
}\
}while(0)

CALL("root",func1(root_path));
CALL("temp",func2(temp_path));

這也是巨集使用的一個典型場景——複用程式碼段。當很多程式碼段都是類似的時候,只有一點點程式碼不同,那麼就可以通過巨集來避免手寫這些重複程式碼。上面這個巨集把不同的程式碼段func1(rootpath),func2(temppath)作為引數傳進來,從而複用這個程式碼段。

我們可以通過一個泛型函式來替換這個巨集:

template<typenameSelf,typenameF>
voidCall(conststd::string&name,Self*self,Ff){
autoret=foo(name);
if(ret==0){
(self>*f)(name);
do_something(name);
}
else{
do_something(name);
}
}

事實上大部分巨集能做的,超程式設計能做得更好、更完美!

介面易用和靈活性

還是以rest_rpc為例,我們可以註冊任意型別的RPC函式,不管引數個數和型別是否相同、返回型別是否相同,這讓我們的註冊介面非常易用和靈活。

structdummy{
intadd(connection*conn,inta,intb){returna+b;}
};

intadd(connection*conn,inta,intb){returna+b;}

rpc_serverserver(8080,4);

dummyd;
server.register_handler("a",&dummy::add,&d);
server.register_handler("b",add);
server.register_handler("c",[](connection*conn){});
server.register_handler("d",[](connection*conn,std::strings){
returns;
});

這裡我們使用超程式設計幫我們擦除了函式型別:

template<typenameFunction>
voidregister_nonmember_func(std::stringconst&name,constFunction&f){
this->map_invokers_[name]={std::bind(&invoker<Function>::apply,f,std::placeholders::_1,std::placeholders::_2,std::placeholders::_3)};
}

template<typenameFunction>
structinvoker{
staticvoidapply(constFunction&func,constchar*data,size_tsize,
std::string&result){
usingargs_tuple=typenamefunction_traits<Function>::args_tuple;
msgpack_codeccodec;
autotp=codec.unpack<args_tuple>(data,size);
call(func,result,tp);
}
};

typename Function做了型別擦除,typename functiontraits::argstuple幫我們還原了型別。

再來看另外一個例子,cinatra(https://github.com/qicosmos/cinatra)註冊路由函式的例子:

server.set_http_handler<GET>("/a",&person::foo);

server.set_http_handler<GET,POST>("/b",&person::foo,log_t{});

server.set_http_handler<GET,POST,HEAD>("/c",&person::foo,log_t{},check{});

server.set_http_handler<GET>("/d",&person::foo,log_t{},check{},enable_cache{false});

server.set_http_handler<GET>("/e",&person::foo,log_t{},enable_cache{false},check{});

server.set_http_handler<POST>("/f",&person::foo,enable_cache{false},log_t{},check{});

這個例子中,使用者可以增加任意切面,還可以增加快取引數,切面和快取引數的順序可以是任意的,這樣完全消除了使用者使用介面時需需要注意引數順序的負擔,完全是自由靈活的。這裡並沒有使用多個過載函式做這個事情,而是藉助超程式設計,把快取引數過濾出來,這樣就可以無視外面傳入引數的順序了。

過濾引數的程式碼如下:

template<http_method...Is,typenameFunction,typename...AP>
voidset_http_handler(std::string_viewname,Function&&f,AP&&...ap){
ifconstexpr(has_type<enable_cache<bool>,std::tuple<std::decay_t<AP>...>>::value){
autotp=filter<enable_cache<bool>>(std::forward<AP>(ap)...);
std::apply(f,std::move(tp));
}
else{
http_router_.register_handler<Is...>(name,std::forward<Function>(f),std::forward<AP>(ap)...);
}
}

template<typenameT,typenameTuple>
structhas_type;

template<typenameT,typename...Us>
structhas_type<T,std::tuple<Us...>>:std::disjunction<std::is_same<T,Us>...>{};

template<typenameT>
structfilter_helper{
staticconstexprautofunc(){
returnstd::tuple<>();
}

template<class...Args>
staticconstexprautofunc(T&&,Args&&...args){
returnfilter_helper::func(std::forward<Args>(args)...);
}

template<classX,class...Args>
staticconstexprautofunc(X&&x,Args&&...args){
returnstd::tuple_cat(std::make_tuple(std::forward<X>(x)),filter_helper::func(std::forward<Args>(args)...));
}
};

這裡通過C++17的std::disjunction來判斷是否存在某個型別,通過if constexpr實現編譯期選擇。

總結

C++新標準給超程式設計帶來了巨大的改變,不僅僅讓超程式設計變得簡單好寫了,還讓它變得更加強大了,幫助我們優雅地解決了很多實際的問題。文中列舉到的超程式設計應用僅僅是冰山一角,還有很多其他方面的應用。

本文內容為作者自 2018 中國 C++大會演講內容整理而來。

作者:祁宇,Modern C++開源社群purecpp.org創始人,《深入應用 C++11》作者,開源庫cinatra、feather作者,熱愛開源,熱愛Modern C++。樂於研究和分享技術,多次在國際C++大會(cppcon)做演講。