從 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(