第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