C++ template —— trait与policy类

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++ template —— trait与policy类相关的知识,希望对你有一定的参考价值。

第15章 trait与policy类
------------------------------------------------------------------------------------------------------------
模板让我们可以针对多种类型对类和函数进行参数,但我们并不希望为了能够最大程度地参数化而引入太多的模板参数,同时在客户端指定所有的相应实参往往也是烦人的。我们知道我们希望引入的大多数额外参数都具有合理的缺省值。在某些情况下额外参数还可以有几个主参数来确定。
policy类和trait(或者称为trait模板)是两种C++程序设计机制。它们有助于对某些额外参数的管理,这里的额外参数是指:在具有工业强度的模板设计中所出现的参数。
trait类:提供所需要的关于模板参数的类型的所有必要信息;(STL源码大量运用了这种技巧)
policy类:有点像策略模式,通过policy类挂接不同的算法;
------------------------------------------------------------------------------------------------------------
15.1 一个实例:累加一个序列
15.1.1 fixed traits

// traits/accum1.hpp

#ifndef ACCUM_HPP
#define ACCUM_HPP

template <typename T>
inline 
T accum(T const* beg, T const* end)
{
    T total  = T();      // 假设T()事实上会产生一个等于0的值
    while(beg != end)
    {
        total += *beg;
        ++beg;
    }    
    return total;
}

#endif //ACCUM_HPP

考虑下面的调用过程:

// traits/accum1.cpp

#include "accum1.hpp"
#include <iostream>

int main()
{
    // 生成一个含有5个整数值的数组
    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;

    // (试图)输出平均的字符值
    std::cout << "the average value of the characters in \"" << name << "\" is "
     << accum(&num[0], &num[length]) / length << \n;
}

输出:
the average value of the integer values is 3
the average value of the characters in "templates" is -5

这里的问题是我们的模板是基于char类型进行实例化的,而char的范围是很小的,即使对于相对较小的数值进行求和也可能会出现越界的情况。显然,我们可以通过引入一个额外的模板参数AccT来解决这个问题,其中AccT描述了变量total的类型(同时也是返回类型)。然而,这将会给该模板的所有用户都强加一个额外的负担:他们每次调用这个模板的时候,都要指定这个额外的类型。因此,针对我们上面的例子,我们不得不这样编写代码:

accum<int>(&name[0], &name[length])

虽然说这个约束并不会很麻烦,但我们仍然期望可以完全避免这个约束。
关于这个额外参数,另一种解决方案是对accum()所调用的每个T类型都创建一个关联,所关联的类型就是用来存储累加和的类型。这种关联可以被看作是类型T的一个特征,因此,我们也把这个存储累加和的类型称为T的trait。于是,我们可以导出我们的第一个trait类:

// traits/accumtraits2.hpp

template<typename T>
class AccumulationTraits;

template<>
class AccumulationTraits<char>
{
    public:
        typedef int AccT;
};

template<>
class AccumulationTraits<char>
{
    public:
        typedef int AccT;
};

template<>
class AccumulationTraits<short>
{
    public:
        typedef int AccT;
};

template<>
class AccumulationTraits<int>
{
    public:
        typedef long AccT;
};

template<>
class AccumulationTraits<unsigned int>
{
    public:
        typedef unsigned long AccT;
};

template<>
class AccumulationTraits<float>
{
    public:
        typedef double AccT;
};

在上面代码中,模板AccumulationTraits被称为一个trait模板,因为它含有它的参数类型的一个trait(通常而言,可以存在多个trait和多个参数)。对这个模板,我们并不提供一个泛型的定义,因为在我们不知道参数类型的前提下,并不能确定应该选择什么样的类型作为和的类型。然而,我们可以利用某个实参类型,而T本身通常都能够作为这样的一个候选类型。这样,我们可以改写前面的accum()模板如下:

// traits/accum2.hpp

#ifndef ACCUM_HPP
#define ACCUM_HPP

template<typename T>
inline
typename AccumulationTraits<T>::AccT accum(T const* beg, T const* end)
{
    // 返回值的类型是一个元素类型的trait
    typedef typename AccumulationTraits<T>::AccT Acct;

    AccT total = AccT();        // 假设AccT()实际上生成了一个0值
    while(beg != end)
    {
        total += *beg;
        ++beg;
    }
    return total;
}

#endif     //  ACCUM_HPP

// 于是,现在例子程序的输入完全符合我们的期望,如下:
the average value of the integer values is 3
the average value of the characters in "templates" is 108

15.1.2 value trait
到目前为止,我们已经看到了trait可以用来表示:“主”类型所关联的一些额外的类型信息。在这一小节里,我们将阐明这个额外的信息并不局限于类型,常数和其他类型的值也可以和一个类型进行关联。
我们前面的accum()模板使用了缺省构造函数的返回值来初始化结果变量(即total),而且我们期望该返回值是一个类似0的值:

AccT total = AccT();       // 假设AccT()实际上生成了一个0值
...
return total;

显然,我们并不能保证上面的构造函数会返回一个符合条件的值,可以用来开始这个求和循环。而且,类型AccT也不一定具有一个缺省构造函数。
在此,我们可以再次使用trait来解决这个问题。对于上面的例子,我们需要给AccumulationTraits添加一个value trait,最终选择的方案如下(书籍介绍了最后选择这种方案的原因,在此省略,详见书籍):

// traits/accumtraits4.hpp

template<typename T>
class AccumulationTraits;

template<>
class AccumulationTraits<char>
{
    public:
        typedef int AccT;
    // 之所以选择使用静态函数返回一个值,原因如下:
    // 方案1:直接定义“static AccT const zero = 0;”,缺点:在所在类的内部,C++只允许我们对整型和枚举类型初始化成静态成员变量
    // 方案2:类内声明“static double const zero;”,源文件进行初始化“double const AccumulationTraits<float>::zero = 0.0;”,
    缺点:这种解决方法对编译器而言是不可知的。也就是说,在处理客户端文件的时候,编译器通常都不会知道位于其他文件的定义
// 综上,选择了下面使用静态函数返回所需要的值的方法 static AccT zero(){ return 0; } }; // 其他内建类型的特化版本类似 ......

对于应用程序代码而言,唯一的区别只是这里使用了函数调用语法(而不是访问一个静态数据成员):

AccT total = AccumulationTraits<T>::zero(); 

显然,trait还可以代表更多的类型。在我们的例子中,trait可以是一个机制,用于提供accum()所需要的、关于元素类型的所有必要信息;实际上,这个元素类型就是调用accum()的类型,即模板参数的类型。下面是trait概念的关键部分:trait提供了一种配置具体元素(通常是类型)的途径,而该途径主要是用于泛型计算。

在上一节所使用的trait被称为fixed trait,因为一旦定义了这个分离的trait,就不能再算法中对它进行改写。然而,在有些情况下我们需要对trait进行改写。从原则上讲,参数化trait主要的目的在于:添加一个具有缺省值的模板参数,而且该缺省值是由我们前面介绍的trait模板决定的。在这种具有缺省值的情况下,许多用户就可以不需要提供这个额外的模板实参;但对于有特殊需求的用户,也可以改写这个预设的类型。

对于这个特殊的解决方案,唯一的不足在于:我们并不能对函数模板预设缺省模板实参。可以通过把算法实现为一个类,绕过这个不足。这同时也说明了:除了函数模板之外,在类模板中也可以很容易地使用trait,唯一的确点就是:类模板不能对它的模板参数进行演绎,而是必须显式提供这些模板参数。因此,我们需要编写如下形式的代码: Accum<char>::accum(&name[0], &name[length]) 前面例子的代码修改如下:

#ifndef ACCUM_HPP
#define ACCUM_HPP

#include "accumtraits4.hpp"

template <typename T,
                typename AT = AccumulationTraits<T> >
class Accum
{
    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;
        }
};
#endif   // ACCUM_HPP

通常而言,大多数使用这个模板的用户都不必显式地提供第2个模板实参,因为我们可以针对第1个实参的类型,为每种类型都配置一个合适的缺省值。
和大多数情况一样,我们可以引入一个辅助函数,来简化上面基于类的接口:

template<typename T>
inline
typename AccumulationTraits<T>::AccT accum(T const* beg, T const* end)
{
    // 第2个实参由类模板的缺省实参提供
    return Accum<T>::accum(beg, end);
}

template<typename Traits, typename T>
inline
typename Traits<T>::AccT accum(T const* beg, T const* end)
{
    // 第2个实参由Traits实参提供,替换缺省实参
    return Accum<T, Traits>::accum(beg, end);
}

15.1.4 policy 和 policy类(个人理解:有点像策略模式,挂接核心操作,改变算法行为)

到目前为止,我们把累积(accumulation)与求和(summation)等价起来了。事实上,还可以有其他种类的累积。例如,我们可以对序列中的给定值进行求积;如果这些值是字符串的话,还可以对它们进行连接。甚至于在一个序列中找到一个最大值,也可以被看成是累积问题的一种形式。在这所有的情况中,针对accum()的所有操作,唯一需要改变的只是“total += *beg;” 操作。于是,我们就把这个操作称为该累积过程的一个policy。因此,一个policy类就是一个提供了一个接口的类,该接口能够在算法中应用一个或多个policy。(个人理解:policy,核心操作的一个代理,通过替换policy,达到改变算法核心操作,从而改变算法行为的目的)

下面是一个例子,它说明了如何在我们的Accum类模板中引入这样的一个接口:

// traits/accum6.hpp

#ifndef ACCUM_HPP
#define ACCUM_HPP

#include "accumtraits4.hpp"
#include "sumpolicy1.hpp"

template <typename T,
                typename Policy = SumPolicy,
                typename Traits = AccumulationTraits<T> >
class Accum
{
    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 //ACCUM_HPP

其中SumPolicy类可以编写如下:

// traits/sumpolicy1.hpp
#ifndef SUMPOLICY_HPP
#define SUMPOLICY_HPP

class SumPolicy
{
    public:
        template<typename T1, typename T2>  // 成员模板
        static void accumulate(T1& total, T2 const & value)
        {
            total += value;
        }
};
#endif //SUMPOLICY_HPP

在这个例子中,我们把policy实现为一个具有一个成员函数模板的普通类(也就是说,类本身不是模板,而且该成员函数是隐式内联的)。后面我们还会讨论另一种实现方案。
通过给累积值指定一个不同的policy,我们就可以进行不同的计算。如下:

// traits/accum7.cpp
#include "accum6.hpp"
#include <iostream>

class MultiPolicy
{
    public:
        template<typename T1, typename T2>
        static void accumulate(T1& total, T2 const & value){
            total *= value;
        }
};

int main()
{
    // 创建含有具有5个整型值的数组
    int num[] = {1, 2, 3, 4, 5};
    // 输出所有值的乘积
    std::cout << "the product of the integer values is " << Accum<int, MultiPolicy>::accum(&num[0], &num[5]) << \n;
}

 

15.1.3 参数化trait

 

以上是关于C++ template —— trait与policy类的主要内容,如果未能解决你的问题,请参考以下文章

traits编程技法

模板Trait 技术与简述template 元编程

对于C++中traits的简单介绍

iterator_trait 的典型用例是啥

模板拾遗二_traits和policy

c++ 迭代器问题