《C++模板元编程实战:一个深度学习框架的初步实现.docx》由会员分享,可在线阅读,更多相关《C++模板元编程实战:一个深度学习框架的初步实现.docx(256页珍藏版)》请在课桌文档上搜索。
1、C+模板元编程实战:一个深度学习框架的初步实现1 .第一部分元编程基础技术2 .第1章基本技巧3 .第2章异类词典与PoliCy模板4 .第3章深度学习概述5 .第4章类型体系与基本数据类型6 .第5章运算与表达式模板7 .第6章基本层8 .第7章复合层与循环层9 .第8章求值与优化10 .后记方家休见笑,吾道本艰难第一部分元编程基础技术第1章基本技巧本章将讨论元编程与编译期计算所涉及的基本方法。我们首先介绍元函数,通过简单的示例介绍编译期与运行期所使用“函数”的异同。其次,在此基础上进一步讨论基本的顺序、分支、循环代码的书写方式。最后介绍一种经典的技巧奇特的递归模板式。上述内容可以视为基本的
2、元编程技术。而本书后续章节也可以视为这些技术的应用。掌握好本章所讨论的技术,是熟练使用C+模板元编程与编译期计算的前提。1.1 元函数与typjtraits1.1.1 元函数介绍C+元编程是一种典型的函数式编程,函数在整个编程体系中处于核心的地位。这里的函数与一般C+程序中定义与使用的函数有所区别,更接近数学意义上的函数是无副作用的映射或变换:在输入相同的前提下,多次调用同一个函数,得到的结果也是相同的。如果函数存在副作用,那么通常是由于存在某些维护了系统状态的变量而导致的。每次函数调用时,即使输入相同,但系统状态的差异会导致函数输出结果不同:这样的函数被称为具有副作用的函数。元函数会在编译期
3、被调用与执行。在编译阶段,编译器只能构造常量作为其中间结果,无法构造并维护可以记录系统状态并随之改变的量,因此编译期可以使用的函数(即元函数)只能是无副作用的函数。以下代码定义了一个函数,满足无副作用的限制,可以作为元函数使用。1 Constexprintfun(inta)returna+1;其中的ConSteXPr为C+11中的关键字,表明这个函数可以在编译期被调用,是一个元函数。如果去掉了这个关键字,那么函数fun将只能用于运行期,虽然它具有无副作用的性质,但也无法在编译期被调用。作为一个反例,考虑如下的程序:2 staticintcall_count=3;3 constexprintfu
4、n2(inta)4 5 returna+(call_count+);6 )这个程序片断无法通过编译它是错误的。原因是函数内部的逻辑丧失了“无副作用”的性质相同输入会产生不同的输出;而关键字ConSteXPr则试图保持函数的“无副作用”特性,这就导致了冲突。将其进行编译会产生相应的编译错误。如果将函数中声明的constexpr关键字去掉,那么程序是可以通过编译的,但fun2无法在编译期被调用,因为它不再是一个元函数了。希望上面的例子能让读者对元函数有一个基本的印象。在C+中,我们使用关键字ConSteXPr来表示数值元函数,这是C+中涉及的一种元函数,但远非全部。事实上,C+中用得更多的是类型元
5、函数即以类型作为输入和(或)输出的元函数。7 .1.2类型元函数从数学角度来看,函数通常可以被写为如下的形式:y=f()其中的3个符号分别表示了输入(X)、输出(y)与映射。通常来说,函数的输入与输出均是数值。但我们大可不必局限于此:比如在概率论中就存在从事件到概率值的函数映射,相应的输入是某个事件描述,并不一定要表示为数值。回到元编程的讨论中,元编程的核心是元函数,元函数输入、输出的形式也可以有很多种,数值是其中的一种,由此衍生出来的就是上一节所提到的数值元函数;也可以将C+中的数据类型作为函数的输入与输出。考虑如下情形:我们希望将某个整数类型映射为相应的无符号类型。比如,输入类型int时,
6、映射结果为UnSignedint;而输入为UnSignedlong时,我们希望映射的结果与输入相同。这种映射也可以被视作函数,只不过函数的输入是int、UnSignedIOng等类型,输出是另外的一些类型而已。可以使用如下代码来实现上述元函数:8 template9 structFun_usingtype=T;1011 template12 structFun-usingtype=unsignedint;1314 template15 structFun-usingtype=unsignedlong;16lFun-:typeh=3;读者可能会问:函数定义在哪儿?最初接触元函数的读者往往会有这样
7、的疑问。事实上,上述片断的18行已经定义了一个函数FlnL第10行则使用了这个Fun_:type函数返回unsignedint1所以第10行相当于定义了一个无符号整型的变量h并赋予值3。Fim_与C+一般意义上的函数看起来完全不同,但根据前文对函数的定义,不难发现,Fun_具备了一个元函数所需要的全部性质: 输入为某个类型信息T,以模板参数的形式传递到Fun_模板中; 输出为Fun_模板的内部类型type.gpFun_:type; 映射体现为模板通过特化实现的转换逻辑:若输入类型为im,则输出类型为UnSignedintl等等。在C+11发布之前,已经有一些讨论C+元函数的著作了。在C+模板元
8、编程一书中,将上述程序段中的16行所声明的FlnL视为元函数:认为函数输入是X时,输出为Fun_:typea同时,该书规定了所讨论的元函数的输入与输出均是类型。将一个包含了type声明的类模板称为元函数,这一点并无不妥之处:它完全满足元函数无副作用的要求。但作者认为,这种定义还是过于狭隘了。当然像这样引入限制,相当于在某种程度上统一了接口,这将带来一些程序设计上的便利性。但作者认为这种便利性是以牺牲代码编写的灵活性为代价的,成本过高。因此,本书对元函数的定义并不局限于上述形式。具体来说:并不限制映射的表示形式一像前文所定义的以COnSteXPr开头的函数,以及本节讨论的提供内嵌type类型的模
9、板,乃至后文中所讨论的其他形式的“函数”,只要其无副作用,同时可以在编译期被调用,都被本书视为元函数; 并不限制输入与输出的形式,输入与输出可以是类型,数值甚至是模板。在放松了对元函数定义的限制的前提下,我们可以在FUk的基础上再引入一个定义,从而构造出另一个元函数FUn:1 template2 usingFun=typenameFun_:type;34 Funh=3;FUn是一个元函数吗?如果按照C+模板元编程中的定义,它至少不是一个标准的元函数,因为它没有内嵌类型type。但根据本章开头的讨论,它是一个元函数,因为它具有输入(T),输出(FUn),同时明确定义了映射规则。那么在本书中,就将
10、它视为一个元函数。事实上,上文所展示的同时也是C+标准库中定义元函数的一种常用的方式。比如,C+11中定义了元函数std:enable_if,而在C+14中引入了定义std:enablejf_t,前者就像Fun_那样,是内嵌了type类型的元函数,后者则就像FUn那样,是基于前者给出的一个定义,用于简化使用。5 .1.3各式各样的元函数在前文中,我们展示了几种元函数的书写方法,与一般的函数不同,元函数本身并非是C+语言设计之初有意引入的,因此语言本身也没有对这种构造的具体形式给出相应的规定。总的来说,只要确保所构造出的映射是“无副作用”的,可以在编译期被调用,用于对编译期乃至运行期的程序行为产
11、生影响,那么相应的映射都可以被称为元函数,映射具体的表现形式则可以千变万化,并无一定之规。事实上,一个模板就是一个元函数。下面的代码片断定义了一个元函数,接收参数T作为输入,输出为Fun:6 template7 structFun;函数的输入可以为空,相应地,我们也可以建立无参元函数:12 3 4 5 6 7structFunusingtype=int;;constexprintfun()8 return1;9这里定义了两个无参元函数。前者返回类型im,后者返回数值10。基于C+14中对Constexpr的扩展,我们可以按照如下的形式来重新定义LLl节中引入的元函数:1 template2 c
12、onsteprintfun=a+1;这看上去越来越不像函数了,连函数应有的大括号都没有了。但这确实是一个元函数。唯一需要说明的是:现在调用该函数的方法与调用LLl节中元函数的方法不同了。对于LLl节的函数,我们的调用方法是fun(3),而对于这个函数,相应的调用方式则变成了funo除此之外,从编译期计算的角度来看,这两个函数并没有很大的差异。前文所讨论的元函数均只有一个返回值。元函数的一个好处是可以具有多个返回值。考虑下面的程序片断:3 template4 structFun-5 6 usingreference_type=int&;7 usingconst_reference_type=co
13、nstint&;8 usingvalue_type=int;9 );这是个元函数吗?希望你回答“是。从函数的角度上来看,它有输入(int),包含多个输出:Fun_:reference_typexFun_:const_reference_type-Fun_:value_type0一些学者反对上述形式的元函数,认为这种形式增加了逻辑间的耦合,从而会对程序设计产生不良的影响(见C+模板元编程)。从某种意义上来说,这种观点是正确的。但作者并不认为完全不能使用这种类型的函数,我们大可不必因噎废食,只需要在合适的地方选择合适的函数形式即可。10 1.4type_traits提到元函数,就不能不提及一个元函
14、数库:type_traitsotype_traits是由boost引入的,C+11将被纳入其中,通过头文件Iypejraits来引入相应的功能。这个库实现了类型变换、类型比较与判断等功能。考虑如下代码:1 std:remove_reference:typehl=3;2 std:remove_reference_th2=3;第1行调用Std:remove_reference这个元函数,将int&变换为im并以之声明了一个变量;第2行则使用std:remove_reference_t实现了相同的功能。std:remove_reference与std:remove_reference都是定义于typ
15、eraits中加元函数,其关家类似于1.1.2节中讨论的FUn_与FUnO通常来说,编写泛型代码往往需要使用这个库以进行类型变换。我们的深度学习框架也不例外:本书会使用其中的一些元函数,并在首次使用某个函数时说明其功能。读者可以参考C+标准模板库等书籍来系统性地了解该函数库。1.1.5 元函数与宏按前文对函数的定义,理论上宏也可以被视为一类元函数。但一般来说,人们在讨论C+元函数时,会把讨论范围的重点限制在ConSteXPr函数以及使用模板构造的函数上,并不包括宏。这是因为宏是由预处理器而非编译器所解析的,这就导致了很多编译期间可以利用到的特性,宏无法利用。典型事例是,我们可以使用名字空间将C
16、OnSteXPr函数与函数模板包裹起来,从而确保它们不会与其他代码产生名字冲突。但如果使用宏作为元函数的载体,那么我们将丧失这种优势。也正是这个原因,作者认为在代码中尽量避免使用宏。但在特定情况下,宏还是有其自身优势的。事实上,在构造深度学习框架时,本书就会使用宏作为模板元函数的一个补充。但使用宏还是要非常小心的。最基本的,作者认为应尽量避免让深度学习框架的最终用户接触到框架内部所定义的宏,同时确保在宏不再被使用时解除其定义。1.1.6 本书中元函数的命名方式元函数的形式多种多样,使用起来也非常灵活。在本书(以及所构造的深度学习框架)中,我们会用到各种类型的元函数。这里限定了函数的命名方式,以
17、使得程序的风格达到某种程度上的统一。在本书中,根据元函数返回值形式的不同,元函数的命名方式也会有所区别:如果元函数的返回值要用某种依赖型的名称表示,那么函数将被命名为xxx_的形式(以下划线为其后缀);反之,如果元函数的返回值可以直接用某种非依赖型的名称表示,那么元函数的名称中将不包含下划线形式的后缀。以下是一个典型的例子:1 template2 structAdd_3 consteprstaticintvalue=a+b;4 );56 template7 consteprintAdd=a+b;89 consteprint1=Add_:value;lconstexprintx2=Add;其中的
18、14行定义了元函数AeIeL;67行定义了元函数Add。它们具有相同的功能,只是调用方式不同:第9与10行分别调用了两个元函数,获取到返回结果后赋予Xl与x2。第9行所获取的是一个依赖型的结果(VahIe依赖于AdcL存在),相应地,被依赖的名称使用下划线作为后缀:Add_;而第10行在获取结果时没有采用依赖型的写法,因此函数名中没有下划线后缀。这种书写形式并非强制性的,本书选择这种形式,仅仅是为了风格上的统1.2模板型模板参数与容器模板相信在阅读了上节之后,读者已经建立起了以下的认识:元函数可以操作类型与数值;对于元函数来说,类型与数值并没有本质上的区别,它们都可视为一种“数据”,可以作为元
19、函数的输入与输出。事实上,C+元函数可以操作的数据包含3类:数值、类型与模板,它们统一被称为“元数据“,以示与运行期所操作的“数据”有所区别。在上一节中,我们看到了其中的前两类,本节首先简单讨论一下模板类型的元数据。1.2.1 模板作为元函数的输入模板可以作为元函数的输入参数,考虑下面的代码:1 templatetemplateclassTl,typenameT22 structFun_3 usingtype=typenameT1:type;4 ;56 templatetemplateclassTl,typenameT27 usingFun=typenameFUn_:type;89 Funh=
20、3;17行定义了元函数Fun,它接收两个输入参数:一个模板与一个类型。将类型应用于模板之上,获取到的结果类型作为返回值。在第9行,使用这个元函数并以std:remove_reference与int&作为参数传入。根据调用规则,这个函数将返回int,即我们在第9行声明了一个M类型的变量h并赋予值3。从函数式程序设计的角度上来说,上述代码所定义的FUn是一个典型的高阶函数,即以另一个函数为输入参数的函数。可以将其总结为如下的数学表达式(为了更明确地说明函数与数值的关系,下式中的函数以大写字母开头,而纯粹的数值则是以小写字母开头):Flm(T1,t2)=1(t2)10 2.2模板作为元函数的输出与数
21、值、类型相似,模板除了可以作为元函数的输入,还可以作为元函数的输出,但编写起来会相对复杂一些。考虑下面的代码:12345678templatestructFun_;templatestructFun-templateusingtype=std:add_lvalue_reference;;9 template10 structFun-11 template12 usingtype=std:remove-reference;13 );1415 template16 template17 usingFun=typenameFun_:!templatetype;1819 template20 usin
22、gRes_=Fun;2122 Res_:typeh=3;代码的113行定义了元函数Fim_:输入为true时,其输出FUn_:IyPe为函数模板addValUeeference,这个函数模板可以为类型增加左值引用;输入为false时,其输出Fun_:type为函数模板remove_reference,这个函数模板可以去除类型中的引用。代码的1517行定义了元函数FUn,与之前的示例类似,FUn是Fun_:type的简写。注意这里的USing用法:为了实现Fim,我们必须引入两层template声明:内层(第16行)的template定义了元函数FUn的模板参数;而外层(第15行)的templa
23、te则表示了Fim的返回值是一个接收一个模板参数的模板一这两层的顺序不能搞错。代码段的1920行是应用元函数FUn计算的结果:输入为false,输出结果保存在Res_中。注意此时的Res_还是一个函数模板,它实际上对应了std:remove_reference这个元函数用于去除类型中的引用。而第22行则是进一步使用这个函数模板(元函数的调用)来声明int型的对象h。如果读者对这种写法感到困惑,难以掌握,没有太大的关系。因为将模板作为元函数输出的实际应用相对较少。但如果读者在后续的学习与工作中遇到了类似的问题,可以将这一小节的内容作为参考。与上一小节类似,这里也将整个的处理过程表示为数学的形式,
24、如下:Fun(addOrRemove)=T其中的addOrRemove是一个bool值,而T则是FIm的输出,是一个元函数。1.2.3容器模板学习任何一门程序设计语言之初,我们通常会首先了解该语言所支持的基本数据类型,比如C+中使用int表示带符号的整数。在此基础上,我们会对基本数据类型进行一次很自然地扩展:讨论如何使用数组。与之类似,如果将数值、类型、模板看成元函数的操作数,那么前文所讨论的就是以单个元素为输入的元函数。在本节中,我们将讨论元数据的“数组”表示:数组中的“元素”可以是数值、类型或模板。可以有很多种方法来表示数组甚至更复杂的结构。C+模板元编程一书讨论了C+模板元编程库MPL(
25、BoostC+templateMeta-Programminglibrary)o它实现了类似STL的功能,使用它可以很好地在编译期表示数组、集合、映射等复杂的数据结构。但本书并不打算使用MPL,主要原因是MPL封装了一些底层的细节,这些细节对于元编程的学习来说,又是非常重要的。如果简单地使用MPL,将在一定程度上丧失学习元编程技术的机会。而另一方面,掌握了基本的元编程方法之后再来看MPL,就会对其有更深入的理解,同时使用起来也会更得心应手。这就好像学习C+语言时,我们通常会首先讨论inta10这样的数组,并以此引申出指针等重要的概念,在此基础上再讨论veor时,就会有更深入的理解。本书会讨论元
26、编程的核心技术,而非一些元编程库的使用方式。我们只会使用一些自定义的简单结构来表示数组,就像Et*这样,简单易用。从本质上来说,我们需要的并非一种数组的表示方式,而是一个容器:用来保存数组中的每个元素。元素可以是数值、类型或模板。可以将这3种数据视为不同类别的操作数,就像C+中的int与float属于不同的类型。在元函数中,我们也可以简单地认为“数值”与“类型”属于不同的类别。典型的C+数组(无论是ini*还是VeCtor)都仅能保存一种类型的数据。这样设计的原因首先是实现比较简单,其次是它能满足大部分的需求。与之类似,我们的容器也仅能保存一种类别的操作数,比如一个仅能保存数值的容器,或者仅能
27、保存类型的容器,或者仅能保存模板的容器。这种容器已经能满足绝大多数的使用需求了。C+11中引入了变长参数模板(Variadictemplate),使用它可以很容易地实现我们需要的容器网:1 templatestructIntContainer;2 templatestructBoolContainer;34 templatestructTypeContainer;56 templatetemplateclass.TstructTemplateCont;7 templatetemplateclass.TstructTemplateCont2;上面的代码段声明了5个容器(相当于定义了5个数组)。其
28、中前两个容器分别可以存放int与bool类型的变量;第3个容器可以存放类型;第4个容器可以存放模板作为其元素,每个模板元素可以接收一个类型作为参数;第5个容器同样以模板作为其元素,但每个模板可以放置多个类型信息。细心的读者可能发现,上面的5条语句实际上是声明而非定义(每个声明的后面都没有跟着大括号,因此仅仅是声明)。这也是C+元编程的一个特点:事实上,我们可以将每条语句最后加上大括号,形成定义。但思考一下,我们需要定义吗?不需要。声明中已经包含了编译器需要使用的全部信息,既然如此,为什么还要引入定义呢?事实上,这几乎可以称为元编程中的一个惯用法了仅在必要时才引入定义,其他的时候直接使用声明即可
29、。在后文中,我们会看到很多类似的声明,并通过具体的示例来了解这些声明的使用方式。事实上,到目前为止,我们已经基本完成了数据结构的讨论深度学习框架只需要使用上述数据结构就可以完成构造了。如果你对这些结构还不熟悉,没关系,在后面构造深度学习框架的过程中,我们会不断地使用上述数据结构,你也就会不断地熟悉它们。数据结构仅仅是故事的一半,一个完整的程序除了数据结构还要包含算法。而算法则是由最基本的顺序、分支与循环操作构成的。在下一节,我们将讨论涉及元函数时,该如何编写相应的顺序、分支或循环逻辑。1.3顺序、分支与循环代码的编写相信本书的读者可以熟练地写出在运行期顺序、分支与循环执行的代码。但本书还是需要
30、单独开辟出一节来讨论这个问题,是因为一旦涉及元函数,相应的代码编写方法也会随之改变。1.3.1 顺序执行的代码顺序执行的代码书写起来是比较直观的,考虑如下代码:1 template2 structRemoveReferenceConst_3 private:4 usinginter_type=typenamestd:remove-reference:type;5 public:6 usingtype=typenamestd:remove_const:type;7 ;89 template10 usingRemoveReferenceConst11 =typenameRemoveReferenc
31、eConst_:type;1213 RemoveReferenceConsth=3;这一段代码的重点是27行,它封装了元函数RemOVeReferenCeConst_,这个函数内部则包含了两条语句,顺序执行:(1)第4行根据T计算出imejtype;第6行根据intejtype计算出type。同时,代码中的Eterjype被声明为PriVate类型,以确保函数的使用者不会误用intejtype这个中间结果作为函数的返回值。这种顺序执行的代码很好理解,唯一需要提醒的是,现在结构体中的所有声明都要看成执行的语句,不能随意调换其顺序。考虑下面的代码:1 structRunTimeExample2 s
32、taticvoidfunl()fun2();3 staticvoidfu2()cerrhelloendl;4 ;这段代码是正确的,可以将fml与fm2的定义顺序发生调换,不会改变它们的行为。但如果我们将元编程示例中的代码调整顺序:1 template2 structRemoveReferenceConst-3 usingtype=typenamestd:remove_const:type;4 usinginter_type=typenamestd:remove-reference:type;5 ;程序将无法编译,这并不难理解:在编译期,编译器会扫描两遍结构体中的代码,第一遍处理声明,第二遍才会
33、深入到函数的定义之中。正因为如此,RlmTimeEXamPle是正确的,第一遍扫描时,编译器只是了解到RUnTimeEXamPIe包含了两个成员函数funl与fun2;在后续的扫描中,编译器才会关注funl中调用了fun2。虽然fun2的调用语句出现在其声明之前,但正是因为这样的两遍扫描,编译器并不会报告找不到fun2这样的错误。但修改后的RemOVeReferenCeCOnSL中,编译器在首次从前到后扫描程序时,就会发现type依赖于一个没有定义的inteuype,它不继续扫描后续的代码,而是会直接给出错误信息。在很多情况下,我们会将元函数的语句置于结构体或类中,此时就要确保其中的语句顺序正
34、确。6 .3.2分支执行的代码我们也可以在编译期引入分支的逻辑。与编译期顺序执行的代码不同的是,编译期的分支逻辑既可以表现为纯粹的元函数,也可以与运行期的执行逻辑相结合。对于后者,编译期的分支往往用于运行期逻辑的选择。我们将在这一小节看到这两种情形各自的例子。事实上,在前面的讨论中,我们已经实现过分支执行的代码了。比如在122节中,实现了一个Fun一元函数,并使用一个bool参数来决定函数的行为(返回值):这就是一种典型的分支行为。事实上,像该例那样,使用模板的特化或部分特化来实现分支,是一种非常常见的分支实现方式。当然,除此之外,还存在一些其他的分支实现方式,每种方式都有自己的优缺点本小节会
35、讨论其中的几种。使用StdrxonditionaI与std:COlKIitionaLt实现分支ConditionaI与Conditional_t是type_traits中提供的两个元函数,其定义如下1:7 namespacestd8 (9 template10 structconditional11 usingtype=T;12 ;1314 template15 structconditional16 usingtype=F;17 );1819 template20 usingconditional_t=typenameconditional:type;21 其逻辑行为是:如果B为真,则函数返
36、回T,否则返回F。其典型的使用方式为:std:ConditionaIVtrue,int,float:type=3;std:conditional_ty=l.f;分别定义了int型的变量X与float型的变量y。COnditiOnal与CCmditiOnaLt的优势在于使用比较简单,但缺点是表达能力不强:它只能实现二元分支(真假分支),其行为更像运行期的问号表达式:x=B?T:F;。对于多元分支(类似于SWitCh的功能)则支持起来就比较困难了。相应地,conditionalconditionalfi使用场景是相对较少的。除非是特别简单的分支情况,否则并不建议使用这两个元函数。使用(部分)特化实
37、现分支在前文的讨论中,我们就是使用特化来实现的分支。(部分)特化天生就是用来引入差异的,因此,使用它来实现分支也是十分自然的。考虑下面的代码:1 structA;structB;23 template4 structFun_5 constexprstaticsize_tvalue=;6 );78 template9 structFun-lconstexprstaticsize_tvalue=1;11 );1213 template14 structFun_15 constexprstaticsize_tvalue=2;16 );1718constexprsize_th=Fun_:value;代
38、码的第18行根据元函数Fun_的输入参数不同,为h赋予了不同的值一这是一种典型的分支行为。Fun_元函数实际上引入了3个分支,分别对应输入参数为A、B与默认的情况。使用特化引入分支代码书写起来比较自然,容易理解,但代码一般比较长。在C+14中,除了可以使用上述方法进行特化,还可以有其他的特化方式,考虑下面的代码:1 structA;structB;23 template4 constexprsize_tFun=0;56 template7 constexprsize_tFun=1;89 template10 constexprsize_tFun=2;1112 constexprsize_th=
39、Fun;这段代码与上一段实现了相同的功能(唯一的区别是元函数调用时,前者需要给出依赖型名称:VaIUe,而后者则无须如此),但实现简单一些。如果希望分支返回的结果是单一的数值,则可以考虑这种方式。使用特化来实现分支时,有一点需要注意:在非完全特化的类模板中引入完全特化的分支代码是非法的。考虑如下代码:13 template14 structWrapper15 template16 structFun_17 constexprstaticsize.tvalue=;18 );1920 template21 structFun_22 constexprstaticsize_tvalue=1;23 ;
40、24 );这个程序是非法的。原因是WraPPer是一个未完全特化的类模板,但在其内部包含了一个模板的完全特化Fun_,这是C+标准所不允许的,会产生编译错误。为了解决这个问题,我们可以使用部分特化来代替完全特化,将上面的代码修改如下:1 template2 structWrapper3 template4 structFun_5 constexprstaticsize_tvalue=;6 );78 template9 structFun-10 constexprstaticsize_tvalue=1;11 );12 );这里引入了一个伪参数TDUmmy,用于将原有的完全特化修改为部分特化。这个
41、参数有一个默认值Void,这样就可直接以Fun_的形式调用这个元函数,无需为伪参数赋值了。使用Stdhenabljif与Stdxenable_if_t实现分支enable_if与enable_if_t的定义如下:12345678911namespacestd(templatestructenable_if;templatestructenable_ifusingtype=T;templateusingenable_if_t=typenameenable-if:type;对于分支的实现来说,这里面的T并不特别重要,重要的是当B为true时,enable_if元函数可以返回结果type。可以基于这
42、个构造实现分支,考虑下面的代码:1 templateboolIsFeedbackOutztypenameTz2 std:enable_if_t*=nullptr3 autoFeedbackOut_(T&7*.*/)45 templateboolIsFeedbackOutztypenameTz6 std:enable_if_t*=nullptr7 autoFeedbaCkoUJ(T&J(*.*/)这里引入了一个分支。当ISFeedbaCke)Ut为真时,std:enable_if_t:Kype是有意义的,这就使得第一个函数匹配成功;与之相应的,第二个函数匹配是失败的。反之,当IsFeedback
43、Out为假时,std:enable_if_t:type是有意义的,这就使得第二个函数匹配成功,第一个函数匹配买败。C+中有一个特性SFINAE(SubstitutionFailureIsNotAnError)1中文译为“匹配失败并非错误”。对于上面的程序来说,一个函数匹配失败,另一个函数匹配成功,则编译器会选择匹配成功的函数而不会报告错误。这里的分支实现也正是利用了这个特性。通常来说,enable_if与enable_if_t会被用于函数之中,用做重载的有益补充重载通过不同类型的参数来区别重名的函数。但在一些情况下,我们希望引入重名函数,但无法通过参数类型加以区分山】。此时通过enabljif与enablejf就能在一定程度上解决相应的重载问题。需要说明的是,enable_if与enable_if_t的使用形式是多种多样的,并不局限于前文中作为模板参数的方式。事实上,只要C+中支持SFINAE的地方,都可以引入enable5或enable_if_tc有兴趣的读者可以参考C+ReferenCe中的说明。enablqif或enable_if_t也是有缺点的:它并不像模板特化那样直观,以之书写的代码阅读起来也相对困难一些(相信了解模板特化机制的程序员比了解SFINAE的还是多一些的)。还要说