服务器之家:专注于服务器技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - C/C++ - 详解c++中的trait与policy模板技术

详解c++中的trait与policy模板技术

2021-11-14 14:05lsgxeva C/C++

trait模板和policy模板技术是把模板的trait和policy这两个针对不同具体类型有变化的方面抽离出来形成两个独立的模板。由于trait和policy本身是模板,它的行为是可配置的,在模板中通过组合或者以模板实参传进来的方式使用trait和po

概述

我们知道,类有属性(即数据)和操作两个方面。同样模板也有自己的属性(特别是模板参数类型的一些具体特征,即trait)和算法策略(policy,即模板内部的操作逻辑)。模板是对有共性的各种类型进行参数化后的一种通用代码,但不同的具体类型又可能会有一些差异,比如不同的类型可能会有自己的不同特征和算法实现策略。

trait模板技术

当在模板代码中需要知道类型参数T的某些特征(比如需要知道T是哪个具体类型,是否有默认构造函数,希望该类型有合理的缺省值,如int型缺省值为0),我们可以声明一个描述T的特征的trait<T>模板,然后对每种具体类型(如int,char,用户定义的类)特化trait<T>,在各特化版本中用typedef为该具体类型(或者想映射成的其他类型)定义统一的别名(比如AliT),根据需要还可指定合理的缺省值等。这样在原来模板文件中#include这个trait<T>模板的文件,就可以在模板代码中使用trait<T>::AliT来获得T的具体特征。

比如我们要计算数组各个元素的累加和,由于数组元素可以是各种类型,我们使用模板来实现它,这时有一个类型参数T。但在算法代码中,某些情况下又必须知道T的具体类型特征,才能作出特殊的处理。例如对char型的数组元素累加如果最终返回的也是char型的话,很可能越界,因为char只占8位,范围很小。我们可以为T的trait创建一个模板AccumulationTraits。具体代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//accum1.hpp:累加算法模板:实现为函数模板,引入了trait。用数组首部指针及尾部后面的一个指针作参数 
#ifndef ACCUM_HPP 
#define ACCUM_HPP 
#include "accumtraits.hpp" 
#include <iostream> 
template<typename T> 
inline typename AccumulationTraits<T>::AccT accum(T const* beg,T const* end){ 
    //返回值类型是要操作的元素类型T的trait 
    typedef typename AccumulationTraits<T>::AccT AccT; 
    AccT total=AccumulationTraits<T>::zero(); //返回具体类型的缺省值 
    while(beg!=end){  //作累加运算 
        total+=*beg; 
        ++beg; 
    
    return total; //返回累加的值 
#endif
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//accumtraits.hpp:累加算法模板的trait 
#ifndef ACCUMTRAITS_HPP 
#define ACCUMTRAITS_HPP 
template<typename T> 
class AccumulationTraits; //只有声明 
//各个特化的定义 
template<> 
class AccumulationTraits<char>{ //把具体类型char映射到int,累加后就返回int 
public
    typedef int AccT;  //统一的类型别名,表示返回类型 
    static AccT zero(){ //关联一个缺省值,是累加时的初始缺省值 
        return 0; 
    
}; 
template<> 
class AccumulationTraits<short>{ //把具体类型short映射到累加后的返回类型int 
public
    typedef int AccT; 
    static AccT zero(){ //没有直接在类内部定义static变量并提供缺省值,而是使用了函数 
                        //因为类内部只能对整型和枚举类型的static变量进行初始化 
                        //其他类型的必须类内部声明,在外部进行初始化 
        return 0; 
    
}; 
template<> 
class AccumulationTraits<int>{ 
public
    typedef long AccT; 
    static AccT zero(){ 
        return 0; 
    
}; 
template<> 
class AccumulationTraits<unsigned int>{ 
public
    typedef unsigned long AccT; 
    static AccT zero(){ 
        return 0; 
    
}; 
template<> 
class AccumulationTraits<float>{ 
public
    typedef double AccT; 
    static AccT zero(){ 
        return 0; 
    
}; 
//... 
#endif
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//accum1test.cpp:使用累加算法的客户端代码 
#include "accum1.hpp" 
#include <iostream> 
int main(){ 
    int num[]={1,2,3,4,5}; //整型数组 
    std::cout<<"the average value of the integer values is "
        <<accum(&num[0],&num[5])/5<<'/n'//输出平均值 
    char name[]="templates"; //创建字符值数组 
    int length=sizeof(name)-1; 
    //输出平均的字符值,返回的是int型,不会越界 
    std::cout<<"the average value of the characters in /"
        <<name<<"/" is "<<accum(&name[0],&name[length])/length<<'/n';  
    return 0; 
}

注意trait模板本身只是一个声明,并不提供定义,因为它并不知道参数T具体是什么类型。trait的定义由针对各个具体类型的特化来提供。trait依赖于原来模板的主参数T,因为它表示的是T的特征信息。这里使用函数zero()为每个具体类型还关联了一个缺省值,用来作为累加的初始值。为什么不直接关联为静态变量呢?比如static AccT const zero=0。这主要是因为在类内部只能对整型和枚举类型的static变量进行初始化,其他类型的必须在类内部声明,在外部进行初始化。这里对char型数组元素进行累加时,返回int型,这样就避免了会产生越界的情况。

总结出trait模板技术的核心思想:把模板参数T的具体特征信息抽象成一个独立的模板,通过特化为具体的类型并为该类型关联统一的别名,我们就可以在模板中引用这个别名,以获得T的具体特征信息。注意一个模板参数可能有多种特征,每一个trait都可以抽象成一个trait模板。可见这里特化是获得具体差异行为的关键。由于在模板中类型T是抽象的,不能获得它的具体特征,我们通过对T的特征进行抽离,并特化为具体的类型,才能获得类型的具体特征。从这可以看出我们还有一种实现方案,那就是直接特化模板accum,即针对char型进行一个特化来进行累加。但这样特化版本中又要重写基本模板中那些相同的代码逻辑(比如进行累加的while循环),而实际上我们需要特化的只是类型的特征信息。

在设计层面上,特化与模板的意图正好相反。模板是泛型代码,代表各个类型之间的共性,而特化则表示各个类型之间的差异。我们可以结合多态来深刻地把握这些设计思想。从一般意义上讲,polymorphism是指具有多种形态或行为,它能够根据单一的标记来关联不同的特定行为。可见条件语句if/else也可以看作是一种多态,它根据标记的不同状态值来选择执行不同的分支代码(代表不同的行为)。多态在不同的程序设计范型有不同的表现。

(1)面向过程的程序设计:多态通过条件语句if/else来实现。这样多态其实成了最基本的程序逻辑结构。我们知道顺序语句和条件语句是最基本的逻辑结构,switch语句本身就是if/else的变体,循环语句相当于有一个goto语句的if/else。这种多态可以称为OP多态,它最大优点就是效率高,只有一个跳转语句,不需要额外的开销。最大缺点就难以扩展,很难应对变化。当有新的行为时,就要修改原来的代码,在if/else中再增加一个分支,然后重新编译代码。它只是一种低层次的多态,需要程序员人工增加代码,判断标记的值。

(2)面向对象程序设计:多态通过虚函数机制,用继承的方式来实现。这里的设计思想就是抽离类型之间的共性,把它们放在基类中,而具体的差异性则放到子类中。我们使用基类指针或引用作为单一的标记,它会自动的绑定到子类对象上,以获得不同的行为。函数重载也可以看作是一种多态,函数名作为单一的标记,我们通过不同的参数类型来调用不同的重载版本,从而获得不同的多态行为。这种多态称为OO多态,它的优点就是自动化,易扩展,提高了复用程度。它不需要程序员人工干预,因为动态绑定是自动进行的。当需要新的行为时,从基类继承一个新的子类即可,不需要修改原来的代码,系统易维护,也易扩展。缺点就是降低了效率,当纵向的继承体系比较深时,要创建大量的对象,虚函数一般也很少能够被内联,这会使内存使用量大幅增加。OO多态是一种高层次的多态,耦合性比OP多态低,但纵向的继承体系仍然有一定的耦合性。

(3)泛型程序设计:多态通过模板来实现。这里的设计思想就是不需要抽离类型之间的共性,而是直接对类型进行参数化,把它设计成模板,以表示共性。类型之间的差异通过特化来实现。编译器会根据类型参数(相当于单一的标记)自动决定是从模板产生实例,还是调用特化的实例。这种多态称为GP多态,它是横向的,代表共性的模板与代表差异性的特化在同一层次上,它们之间是相互独立的,因此它的耦合性更低,性能也更好。由于GP本身也支持继承和重载,因此可以看出它是一种更高层次的多态,而用模板来做设计甚至比面向对象设计还强大,因为模板本身也支持面向对象的继承机制,它在面向对象层次上还作了一层更高的抽象(对类进行抽象)。GP多态还具有更好的健壮性,因为它在编译期就进行检查。当然,GP代码比较难调试,这主要由于 编译器支持得不好。

用模板参数来传递多种trait

前面我们在accum中通过组合的方式使用它的trait模板。我们也可直接给accum模板增加一个模板参数用来传递trait类型,并指定一个缺省实参为AccumulationTraits<T>,这样可以适应有多种trait的情况。由于函数模板并不能指定缺省模板实参(其实现在许多编译器都支持这个非标准特性),我们把accum实现为一个类模板。算法作为一个函数来使用时应该会更自然一点,因此可以再用一个函数模板来包装这个类模板,使之变成一个函数模板。如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//accum2.hpp:累加算法模板:实现为类模板,用模板参数来传递trait 
//可用一个内联函数模板作为包装器来包装这个类模板实现 
#ifndef ACCUM_HPP 
#define ACCUM_HPP 
#include "accumtraits.hpp" 
template<typename T,typename AT=AccumulationTraits<T> > 
class Accum{ //实现为类模板,模板参数AT代表要使用的trait,并有一个缺省实参 
public
    static typename AT::AccT accum(T const* beg,T const* end){ 
        typename AT::AccT total=AT::zero(); //获取缺省值 
        while(beg != end){ //进行累加 
            total+=*beg; 
            ++beg; 
        
        return total; //返回累加的值 
    
}; 
//用内联的函数模板来包装,对默认的trait,使用一个独立的重载版本 
template<typename T> 
inline typename AccumulationTraits<T>::AccT accum(T const* beg,T const* end){ 
    return Accum<T>::accum(beg,end); 
template<typename T,typename Traits> 
inline  
typename Traits::AccT accum(T const* beg,T const* end){ 
    return Accum<T,Traits>::accum(beg,end); 
#endif

使模板参数来传递trait的一个最大好处是当有多种trait时,我们可以为第2个模板参数指定需要的各种trait。这里还使用了所谓的内联包装函数技术。当我们实现了一个函数(模板),但接口比较难用时,比如这里是类模板,用户即使是使用默认的AccumumationTrait<T>,也要显式指定第一个实参T,不好用。我们可以用一个包装函数来包装它,使其接口变得对用户非常简单友好,为了避免包装带来的性能损失,要把包装函数(模板)声明为内联,编译器通常会直接调用位于内联函数里面的那个函数。这样,使用默认trait时客户端代码accum1test.cpp不需要做任何修改。

policy模板技术

与trait模板技术的思想类似,只不过是对模板代码中的算法策略进行抽离。因为模板代码中对不同的具体类型可能某一部分代码逻辑(即算法策略)会不一样(比如对int是累加,对char则是连接)。policy模板就代表了这些算法策略。它不需要使用特化,policy只需重新实现这个与原模板中的代码不同的具体算法策略即可。

上面是对类型的不同trait产生的差异。实际上对不同的trait,其算法策略(policy)也可能有不同的差异。比如我们对char型元素的数组,不用累加策略,而是用连接的策略。我们还可以把accum看作是一般的数组元素累积性函数,既可以累加,也可以累乘、连接等。一种方法是我们可以直接对accum函数模板的不同具体类型提供特化,重写各自的代码逻辑。但实际上,这时我们需要变化的只有total+=*beg那一条语句,因此我们可以使用policy模板技术,为模板的不同policy创建独立的模板。这里我们把policy实现为具有一个成员函数模板的普通类(当然policy也可以直接实现为模板)。对累加策略为SumPolicy,对累乘策略为MultPolicy等。代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//policies1.hpp:累加元素模板的不同policy实现:实现为含有成员函数模板的普通类 
#ifndef POLICIES_HPP 
#define POLICIES_HPP 
class SumPolicy{ //累加的policy 
public
    template<typename T1,typename T2> 
    static void accumulate(T1& total,T2 const& value){ 
        total+=value; //作累加 
    
}; 
class MultPolicy{ //累乘的policy 
public
    template<typename T1,typename T2> 
    static void accumulate(T1& total,T2 const& value){ 
        total*=value; 
    
}; 
//其他各种policy 
//...... 
#endif

引入了policy后,把累加算法实现为类模板,如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//accum3.hpp:累加算法模板,引入了作为普通类的policy,默认是采用SumPolicy 
#ifndef ACCUM_HPP 
#define ACCUM_HPP 
#include "accumtraits.hpp" 
#include "policies1.hpp" 
template<typename T,typename Policy=SumPolicy,typename Traits=AccumulationTraits<T> > 
class Accum{ //累加算法实现为类模板,默认采用SumPolicy 
public
    typedef typename Traits::AccT AccT; 
    static AccT accum(T const* beg,T const* end){ 
        AccT total=Traits::zero();  //获取缺省值 
        while(beg !=end){ //作累积运算 
            Policy::accumulate(total,*beg); //使用给定的算法策略来进行累积 
            ++beg; 
        
        return total; //返回累积起来的值 
    
}; 
#endif

当policy为普通类时,这里用一个类型模板参数来传递不同的policy,缺省的policy为SumPolicy。客户端使用Accum<int>::accum(&num[0],&num[5])这样的形式来对int型数组元素进行累加。注意当trait使用默认的AccummulationTrait<T>时,累乘策略MultPolicy实际上就不能用在这里了。因为初始值为0,那累乘的结果最终总是0,可见policy与trait是有联系的。当然我们也可以换一种方法来实现,即直接让accum函数增加一个形参T val,用val来指定运算的初始值。实际上,C++标准库函数accumulate()就是把这个初值作为第3个实参。

模板化的policy

上面的policy实现为具有一个成员函数模板的普通类,这可以看出,其实policy可以直接实现为一个模板。这时在accum算法中就要用模板模板参数来传递policy了。代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
//policies2.hpp:把各个policy实现为类模板 
#ifndef POLICIES_HPP 
#define POLICIES_HPP 
template<typename T1,typename T2> 
class SumPolicy{ 
public
    static void accumulate(T1& total,T2 const& value){ 
        total+=value; 
    
}; 
//... 
#endif
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//accum4.hpp:累加算法模板,引入了作为类模板的policy,默认是采用SumPolicy 
#ifndef ACCUM_HPP 
#define ACCUM_HPP 
#include "accumtraits.hpp" 
#include "policies2.hpp" 
template<typename T, 
    template<typename,typename> class Policy=SumPolicy, 
    typename Traits=AccumulationTraits<T> > 
class Accum{ //累加算法实现为类模板,默认采用模板SumPolicy 
public
    typedef typename Traits::AccT AccT; //获取返回类型,它是T的trait 
    static AccT accum(T const* beg,T const* end){ 
        AccT total=Traits::zero();  //获取缺省值 
        while(beg !=end){ //作累积运算 
            Policy<AccT,T>::accumulate(total,*beg); //使用给定的算法策略来进行累积 
            ++beg; 
        
        return total; //返回累积起来的值 
    
}; 
#endif

trait模板与policy模板技术的比较

(1)trait注重于类型,policy更注重于行为。

(2)trait可以不通过模板参数来传递,它表示的类型通常具有自然的缺省值(如int型为0),它依赖于一个或多个主参数,它 一般用模板来实现。

(3)policy可以用普通类来实现,也可以用类模板来实现,一般通过模板参数来传递。它并不需要类型有缺省值,缺省值通常是在policy中的成员函数中用一个独立的参数来传递。它通常并不直接依赖于模板参数。

一般在模板中指定两个模板参数来传递trait和policy。而policy的种类更多,使用更频繁,因此通常代表policy的模板参数在代表trait的模板参数前面。

标准库中的std::iterator_traits<T>是一个trait,可通过iterator_traits<T>::value_ type来引用T表示的具体类型。其实现也是用特化来获取各个具体的类型,有全局特化也有局部物化,如指针类型,引用类型等就只能通过局部特化为T*,T&来实现。

以上就是详解c++中的trait与policy模板技术的详细内容,更多关于c++中的trait与policy模板技术的资料请关注服务器之家其它相关文章!

原文链接:https://www.cnblogs.com/lsgxeva/p/7700701.html

延伸 · 阅读

精彩推荐