首页 > 编程语言 > 详解c++中的trait与policy模板技术
2021
08-22

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

概述

我们知道,类有属性(即数据)和操作两个方面。同样模板也有自己的属性(特别是模板参数类型的一些具体特征,即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。具体代码如下:

//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  
//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  
//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实现为一个类模板。算法作为一个函数来使用时应该会更自然一点,因此可以再用一个函数模板来包装这个类模板,使之变成一个函数模板。如下:

//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等。代码如下:

//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后,把累加算法实现为类模板,如下:

//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了。代码如下:

//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  
//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模板技术的资料请关注自学编程网其它相关文章!

编程技巧