通过trait,可以根据某些类型来定义某种行为(policy)。
一个实例:累加一个序列
fixed traits
假设:
- 所要计算总和的值都是存储在一个数组里面的
- 目前已经有一个指向数组的指针,一个指向数组最后一个元素的后一位的指针,这两个指针之间的所有元素就是要进行求总和的元素
#ifndef UNTITLED_ACCUM1_H
#define UNTITLED_ACCUM1_H
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 //UNTITLED_ACCUM1_H
问题:如果为正确的类型生成了0值?
- 对于内建数值类型(比如int), T()会生成0
- 其他类型
#include <iostream>
#include <accum1.h>
using namespace std;
int main()
{
int num[] = {1, 2, 3, 4, 5};
std::cout << "the average value of the integer value is "
<< accum(&num[0], &num[5]) / 5 << "\n";
char name[] = "templates";
int length = size(name) - 1;
std::cout << "the average value of the characters value is "
<< accum(&num[0], &num[length]) / length << "\n";
}
前半部分对,后半部分明显错了:这是因为模板时基于char类型进行实例化的,结果加起来超过了char可以表示的范围。
解决方法
- 第一种:引入一个额外的模板参数AccT(表示变量total的类型和返回类型),但是这会给该模板的所有用户都加上一个额外的负担:每次调用这个模板的时候,都要指定这个额外的参数
- 第二种:对accum()所调用的每个T类型都创建一个关联,所关联的类型就是用来存储累加和的类型。这种关联可以被看作类型T的一个特征,因此,我们也罢这个存储累加和的类型称为T的trait。于是,我们可以使用模板特化来写出这些关联代码
// accumtraits.h
#ifndef UNTITLED_ACCUMTRAITS_H
#define UNTITLED_ACCUMTRAITS_H
template<typename T>
class AccumulationTraits;
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;
};
#endif //UNTITLED_ACCUMTRAITS_H
上面代码中,模板AccumulationTraits被称为一个trait模板,因为它含有它的参数类型的一个trait(通常而言,可以存在多个trait和多个参数)对这个模板,我们并不提供一个泛型的定义,因为在我们不知道参数类型的前提下, 并不能确定应该选择什么样的类型作为和的类型。然而,我们可以利用某个实参类型,而T本身通常都能够作为这样的一个候选类型。
让我们来改写前面的accum()模板:
#ifndef UNTITLED_ACCUM1_H
#define UNTITLED_ACCUM1_H
#include "accumtraits.h"
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 //UNTITLED_ACCUM1_H
运行main函数,生成正确:
总体来说,上面的修改增加了一个非常有用的机制,从而可以自定义我们的算法。进一步来说,如果有新的类型要使用accum()模板,那么只需要声明AccumulationTraits模板的一个新的显示特化来关联Acct和该类型即可。另外,任何类型都可以和Acct进行关联,来实现这个trait。
value trait
到目前位置,trait可以用来表示:‘主’类型所关联的一些额外的类型信息。本节中将说明这个额外的信息并不局限于类型,常数和其他类型的值也可以和一个类型进行关联
在上面的例子中,accum()模板使用了缺省构造函数的返回值来初始化结果变量(即total),而且我们期望该返回值是一个类似0的值
AccT total = AccT(); // 假设AccT()实际上生成了一个0值
显然,我们并不能保证上面的构造函数会返回一个符合条件的值,而且,类型Acct也不一定具有一个缺省构造函数。
对此,我们为AccumulationTraits添加一个value trait来解决这个问题:
#ifndef UNTITLED_ACCUMTRAITS_H
#define UNTITLED_ACCUMTRAITS_H
template<typename T>
class AccumulationTraits;
template<>
class AccumulationTraits<char>{
public:
typedef int AccT;
static AccT const zero = 0;
};
template<>
class AccumulationTraits<short>{
public:
typedef int AccT;
static AccT const zero = 0;
};
template<>
class AccumulationTraits<int>{
public:
typedef long AccT;
static AccT const zero = 0;
};
template<>
class AccumulationTraits<unsigned int>{
public:
typedef unsigned long AccT;
static AccT const zero = 0;
};
template<>
class AccumulationTraits<float>{
public:
typedef double AccT;
constexpr static AccT const zero = 0.0;
};
#endif //UNTITLED_ACCUMTRAITS_H
在上面的代码中,新trait是一个常量,而常量是在编译期求值的。因此,模板修改如下:
#ifndef UNTITLED_ACCUM1_H
#define UNTITLED_ACCUM1_H
#include "accumtraits.h"
template<typename T>
inline
typename AccumulationTraits<T>::AccT accum(T const * beg, T const *end){
typedef typename AccumulationTraits<T>::AccT AccT;
AccT total = AccumulationTraits<T>::zero;
while (beg != end){
total += *beg;
++beg;
}
return total;
}
#endif //UNTITLED_ACCUM1_H
在上面的代码中,total的初始化是这样的:
AccT total = AccumulationTraits<T>::zero;
但是,这个方案有一个缺点:在所在类的内部,C++只允许我们对整型和枚举类型初始化成静态成员变量,而对于比如浮点型等其他类型就不能用上面的方案,比如下面的特化是错误的
对于这个问题,一个直接的解决方案就是不在所在类的内部定义这个value trait, 而在类外进行初始化,如下
不推荐这种做法,我们趋向于实现下面这种value trait,而且并不需要保证内联成员函数返回的必须是整形值:
#ifndef UNTITLED_ACCUMTRAITS_H
#define UNTITLED_ACCUMTRAITS_H
template<typename T>
class AccumulationTraits;
template<>
class AccumulationTraits<char>{
public:
typedef int AccT;
static AccT zero(){
return 0;
} ;
};
template<>
class AccumulationTraits<short>{
public:
typedef int AccT;
static AccT zero(){
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 //UNTITLED_ACCUMTRAITS_H
模板改成:
#ifndef UNTITLED_ACCUMTRAITS_H
#define UNTITLED_ACCUMTRAITS_H
template<typename T>
class AccumulationTraits;
template<>
class AccumulationTraits<char>{
public:
typedef int AccT;
static AccT zero(){
return 0;
} ;
};
template<>
class AccumulationTraits<short>{
public:
typedef int AccT;
static AccT zero(){
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 //UNTITLED_ACCUMTRAITS_H
很明显,trait可以代表更多的类型。trait可以是一个机制,用于提供accum()所需要的,关于元素类型的所有必要信息:实际上,这个元素类型就是调用accum()的类型,即模板参数的类型。下面是trait概念的关键部分:trait提供了一个配置具体元素(通常是类型)的途径,而该途径主要用于泛型计算
参数化trait
上面所说的trait被称为fixed trait,因为一旦定义了这个分类的trait,就不能在算法中对它进行改写。
原则上讲,参数化trait主要的目的在于:添加一个具有缺省值的模板参数,而且该缺省值是由我们前面所介绍的trait模板决定的。在这种具有缺省值的情况下,许多用户就可以不需要提供这个额外的模板实参,但是对于有特殊需求的用户,也可以改写这个预设的和类型。
#ifndef UNTITLED_ACCUM1_H
#define UNTITLED_ACCUM1_H
#include "accumtraits.h"
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 //UNTITLED_ACCUM1_H
#include <iostream>
#include <accum1.h>
using namespace std;
int main()
{
int num[] = { 1, 2, 3, 4, 5 };
std::cout << "the average value of the integer value is "
<< Accum<int>::accum(&num[0], &num[5]) / 5 << "\n";
char name[] = "templates";
int length = size(name) - 1;
std::cout << "the average value of the characters value is "
<< Accum<char>::accum(&name[0], &name[length]) / length << "\n";
}
policy和policy类
在上面的例子中,我们对序列中的给定值进行了求和,实际上,我们还可以对它们进行求积;如果这些值是字符串的话,还可以对它们进行连接等操作。在这所有的情况中,针对accum()的所有操作,唯一需要改变的只是total += *beg()
。于是我们把这个操作称为accum()过程中的一个policy。因此,一个policy类就是一个提供了一个接口的类,该接口能够在算法中应用一个或者多个policy。
我们先来用policy改写上面的操作。引入一个SumPolicy类:
#ifndef UNTITLED_SUMPOLICY_H
#define UNTITLED_SUMPOLICY_H
class SumPolicy{
public:
template<typename T1, typename T2>
static void accumulate(T1& total, T2 const & value){
total += value;
}
};
#endif //UNTITLED_SUMPOLICY_H
在上面中,我们把policy中实现了一个具有成员函数模板的普通类(也就是说,类本身不是模板,而且该成员函数是隐式内联的)。然后我们在accum模板中使用它:
#ifndef UNTITLED_ACCUM1_H
#define UNTITLED_ACCUM1_H
#include "accumtraits.h"
#include "sumpolicy.h"
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 //UNTITLED_ACCUM1_H
使用:
#include <iostream>
#include <accum1.h>
using namespace std;
int main()
{
int num[] = { 1, 2, 3, 4, 5 };
std::cout << "the average value of the integer value is "
<< Accum<int, SumPolicy>::accum(&num[0], &num[5]) / 5 << "\n";
char name[] = "templates";
int length = size(name) - 1;
std::cout << "the average value of the characters value is "
<< Accum<char>::accum(&name[0], &name[length]) / length << "\n";
}
我们可以指定其他的Policy,就可以进行不同的计算。下面我们来看一个例子:
#include <iostream>
#include <accum1.h>
using namespace std;
class MultPolicy {
public:
template<typename T1, typename T2>
static void accumulate (T1& total, T2 const& value) {
total *= value;
}
};
int main()
{
int num[] = { 1, 2, 3, 4, 5 };
std::cout << "the average value of the integer value is "
<< Accum<int, MultPolicy>::accum(&num[0], &num[5]) / 5 << "\n";
}
结果不对:
这是因为对初始值选择造成的:求积时初始值不能为0. 这个现象说明了:不同的trait和不同的policy应该是相互交互的,我们应该用更加谨慎的态度来对待模板设计
另:并不是所有的问题都必须有trait和policy来解决的。比如,C++标准库的accumulate函数就把这个初值作为函数调用的第三个实参
成员模板和模板的模板参数
上面我们把SumPolicy和MultPolicy实现为具有成员模板的普通类。另外,我们也可以使用类模板来设计这个policy class接口,而这个policy class也就被用作模板的模板实参:
#ifndef UNTITLED_SUMPOLICY_H
#define UNTITLED_SUMPOLICY_H
template<typename T1, typename T2>
class SumPolicy{
public:
static void accumulate(T1& total, T2 const & value){
total += value;
}
};
#endif //UNTITLED_SUMPOLICY_H
为使用模板的模板参数,我们对accum做如下修改:
#ifndef UNTITLED_ACCUM1_H
#define UNTITLED_ACCUM1_H
#include "accumtraits.h"
#include "sumpolicy.h"
template <typename T,
template<typename , typename > class 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<AccT, T>::accumulate(total, *beg);
++beg;
}
return total;
}
};
#endif //UNTITLED_ACCUM1_H
使用模板:
#include <iostream>
#include <accum1.h>
int main()
{
int num[] = { 1, 2, 3, 4, 5 };
std::cout << "the average value of the integer value is "
<< Accum<int>::accum(&num[0], &num[5]) / 5 << "\n";
}
使用模板的模板参数访问policy class的优缺点:
- 优点:借助于某个依赖于模板参数的类型,就可以很容易的让policy class携带一些状态信息(也就是静态成员变量)
- 缺点:policy类必须被写成模板,而且接口中还定义了模板参数的确切个数,这个定义会让我们无法在policy中添加额外参数。如果如果我们希望给SumPolicy添加一个Boolean型的非类型模板实参,从而选择使用+=求和或者+求和,如果是第一种方案就可以这样写:
#ifndef SUMPOLICY_HPP
#define SUMPOLICY_HPP
template<bool use_compound_op = true>
class SumPolicy {
public:
template<typename T1, typename T2>
static void accumulate (T1& total, T2 const & value) {
total += value;
}
};
template<>
class SumPolicy<false> {
public:
template<typename T1, typename T2>
static void accumulate (T1& total, T2 const & value) {
total = total + value;
}
};
#endif // SUMPOLICY_HPP
而第2种方案就不能做这样的修改
组合多个trait 、 poliy
trait和policy通常都不能完全代替多个模板参数,但是它们可以减少参数的个数,并把个数限制在可控制的范围内。那如何对这些参数进行排序呢?
一个简单的策略就是根据缺省值使用频率递增的对各个参数进行排序。这就意味着:trait参数将位于 policy参数的后面,因为客户端代码中通常都会修改policy参数
运用普通迭代器进行累积
#ifndef UNTITLED_ACCUM1_H
#define UNTITLED_ACCUM1_H
#include <iterator>
template<typename Iter>
inline
typename std::iterator_traits<Iter>::value_type
accum(Iter start, Iter end){
typedef typename std::iterator_traits<Iter>::value_type VT;
VT total = VT();
while (start != end){
total += *start;
start++;
}
return total;
}
#endif //UNTITLED_ACCUM1_H
上面accum()仍然允许我们使用指针来调用accum():
#include <iostream>
#include <accum1.h>
int main()
{
int num[] = { 1, 2, 3, 4, 5 };
std::cout << "the average value of the integer value is "
<< accum(&num[0], &num[5]) / 5 << "\n";
}
这是因为C++标准库提供了iterator_trait。iterator_trait结构封装了迭代器的所有相关属性。由于存在一个适用于指针的局部特化,所以普通指针类型也能使用这些trait:
namespace std{
template<typename T>
struct iterator_traits<T*>{
typedef T value_type;
typedef ptrdiff_t difference_type;
typedef random_access_iterator_tag iterator_category;
typedef T* pointer;
typedef T& reference;
};
}
trait和policy的区别
policy更加注重行为,trait更加注重类型。
- trait表示模板参数一些自然的额外属性
- policy表示泛型函数和泛型类的一些可配置行为(通常具有被经常使用的缺省值)
相对于trait
- trait可以是fixed trait(也就是说,不需要通过模板参数进行传递的trait)
- trait参数通常都具有很自然的缺省值(该缺省值很少会被改写,或者是不能改写的)
- trait参数可以紧密依赖于一个或者多个主参数
- trait通常都是由trait模板实现的
对于policy class
- 如果不以模板参数的形式进行传递的话,policy class几乎不起作用
- policy参数并不需要缺省值,而且通常都是显示指定这个参数(尽管很多泛型组件都配置了使用频率很高的缺省policy)
- policy参数和属于同一个模板的其他参数通常都是正交的
- policy class 一般都包含了成员函数
- policy既可以使用普通类实现,也可以使用模板实现