第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[] = {,,,,};
// 输出平均值
std::cout << "the average value of the integer values is" << accum(&num[], &num[]) / << '\n'; // 创建字符值数组
char name[] = "templates";
int length = sizeof(name) - ; // (试图)输出平均的字符值
std::cout << "the average value of the characters in \"" << name << "\" is "
<< accum(&num[], &num[length]) / length << '\n';
} 输出:
the average value of the integer values is
the average value of the characters in "templates" is -
这里的问题是我们的模板是基于char类型进行实例化的,而char的范围是很小的,即使对于相对较小的数值进行求和也可能会出现越界的情况。显然,我们可以通过引入一个额外的模板参数AccT来解决这个问题,其中AccT描述了变量total的类型(同时也是返回类型)。然而,这将会给该模板的所有用户都强加一个额外的负担:他们每次调用这个模板的时候,都要指定这个额外的类型。因此,针对我们上面的例子,我们不得不这样编写代码:
accum<int>(&name[], &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
the average value of the characters in "templates" is
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 ;
}
};
// 其他内建类型的特化版本类似
......
对于应用程序代码而言,唯一的区别只是这里使用了函数调用语法(而不是访问一个静态数据成员):
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[] = {, , , , };
// 输出所有值的乘积
std::cout << "the product of the integer values is " << Accum<int, MultiPolicy>::accum(&num[], &num[]) << '\n';
}
15.1.3 参数化trait
在上一节所使用的trait被称为fixed trait,因为一旦定义了这个分离的trait,就不能再算法中对它进行改写。然而,在有些情况下我们需要对trait进行改写。从原则上讲,参数化trait主要的目的在于:添加一个具有缺省值的模板参数,而且该缺省值是由我们前面介绍的trait模板决定的。在这种具有缺省值的情况下,许多用户就可以不需要提供这个额外的模板实参;但对于有特殊需求的用户,也可以改写这个预设的类型。
对于这个特殊的解决方案,唯一的不足在于:我们并不能对函数模板预设缺省模板实参。可以通过把算法实现为一个类,绕过这个不足。这同时也说明了:除了函数模板之外,在类模板中也可以很容易地使用trait,唯一的确点就是:类模板不能对它的模板参数进行演绎,而是必须显式提供这些模板参数。因此,我们需要编写如下形式的代码:
Accum<char>::accum(&name[], &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[] = {, , , , };
// 输出所有值的乘积
std::cout << "the product of the integer values is " << Accum<int, MultiPolicy>::accum(&num[], &num[]) << '\n';
}
// 程序的输出结果却出乎我们意料:
the product of the integer values is
显然,这里的问题是我们对初始值的选择不当所造成的:因为对于求和,0是一个合适的初值;但对于求积,0却是一个错误的初值。可以在policy实现zero()的trait,也可以把这个初值作为参数传递进来。
15.1.5 trait和policy:区别在何处
大多数人接受Andrei Alexandrescu在Modern C++ Design中给出的声明:
policyhe trait具有许多共同点,但是policy更加注重于行为,而trait则更加注重于类型。
另外,作为引入了trait技术的第1人,Nathan Myers给出了下面这个更加开放的定义:
trait class:是一种用于代替模板参数的类。作为一个类,它可以是有用的类型,也可以是常量;作为一个模板,它提供了一种实现“额外层次间接性”的途径,而正是这种“额外层次间接性”解决了所有的软件问题。
因此,我们通常会使用下面这些(并不是非常准确的)定义:
(1)trait表述了模板参数的一些自然的额外属性;
(2)policy表述了泛型函数和泛型类的一些可配置行为(通常都具有被经常使用的缺省值)。
为了更深入地分析这两个概念之间可能的区别,我们给出下面针对trait的一些事实:
(1)trait可以是fixed trait(也就是说,不需要通过模板参数进行传递的trait)。
(2)trait参数通常都具有很自然的缺省值(该缺省值很少会被改写的,或者是不能被改写的)。
(3)trait参数可以紧密依赖于一个或多个主参数。
(4)trait通常都是用trait模板来实现的。
对于policy class,我们将会发现下列事实:
(1)如果不以模板参数的形式进行传递的话,policy class几乎不起作用。
(2)policy 参数并不需要具有缺省值,而且通常都是显式指定这个参数(尽管许多泛型组件都配置了使用频率很高的缺省policy)。
(3)policy参数和属于同一个模板的其他模板参数通常都是正交的。
(4)policy class一般都包含了成员函数。
(5)policy既可以用普通类来实现,也可以用类模板来实现。
15.1.6 成员模板和模板的模板参数
为了实现一个累积policy,在前面我们选择把Sumpolicy和MutPolicy实现为具有成员模板的普通类。另外,还存在另一种实现方法,即使用类模板来设计这个policy class接口,而这个policy class也就被用作模板的模板实参。如下:
// traits/sumpolicy2.hpp #ifndef SUMPOLICY_HPP
#define SUMPOLICY_HPP
template <typename T1, typename T2>
class SumPolicy
{
public:
static void accumulate (T1& total, T2 const & value)
{
total += value;
}
};
#endif //SUMPOLICY_HPP
于是,可以对Accum的接口进行修改,从而使用一个模板的模板参数,如下:
// traits/accum8.hpp #ifndef ACCUM_HPP
#define ACCUM_HPP #include "accumtraits4.hpp"
#include "sumpolicy2.hpp" 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 // ACCUM_HPP
我们也可以不把AccT类型显式地传递给policy类型,而是只传递上面的累积trait,并且根据这个trait参数来确定返回结果的类型,而且这样做在某些情况下(诸如需要给trait其他的一些信息)是有利的。
通过模板的模板参数访问policy class的主要优点在于:借助于某个依赖于模板参数的类型,就可以很容易地让policy class携带一些状态信息(也就是静态成员变量)。而在我们的第1种解决方案中,却不得不把静态成员变量嵌入到成员类模板中。
然而,这种利用模板的模板参数的解决方案也存在一个缺点:policy类现在必须被写成模板,而且我们的接口中还定义了模板参数的确切个数。遗憾的是,这个定义会让我们无法在policy中添加额外的模板参数。例如,我们希望给SumPolicy添加一个Boolean型的非类型模板实参,从而可以选择是用 += 运算符来进行求和,还是只用 + 运算符来进行求和。在这个例子中,如果我们使用的是前面( traits/accum6.hpp)的成员模板,那么只需要这样更改SumPolicy模板即可:
// traits/sumpolicy3.hpp
#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
然而,如果我们使用模板的模板参数来实现上面的Accum,那么将不能做这样的修改。
15.1.7 组合多个policie和/或 trait
从我们上面的开发过程可以看出,trait和policy通常都不能完全代替多个模板参数;然而,trait和policy确实可以减少模板参数的个数,并把个数限制在可控制的范围以内。一种简单的策略就是根据缺省值使用频率递增地对各个参数进行排序。显然,这意味着:trait参数将位于policy参数的后面(即右边),因为我们在客户端代码中通常都会对policy参数进行改写。
15.1.8 运用普通的迭代器进行累积
这里直接给出代码,STL源码中比较多的使用了这个用法:
// traits/accum0.hpp
#ifndef ACCUM_HPP
#define ACCUM_HPP #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(); // 假设VT()实际上生成了一个0值
while (start != end)
{
total += *start;
++start;
}
return total;
}
#endif // ACCUM_HPP
iterator_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;
};
}
然而,由于迭代器所引用的类型并不能表示累积值的类型,因此我们仍然需要自己设计AccumulationTraits。
15.2 类型函数
通过前面的trait例子,我们知道可以根据某些类型来定义某种行为。这与我们通常在程序设计中实现是不同的。在C和C++中,更准确而言,函数可以被称为函数:函数接收的参数是某些值,而且函数的返回结果也是值。现在,我们要说明的是类型函数:一个接收某些类型实参,并且生成一个类型作为函数的返回结果。
sizeof就是一个非常有用的、内建的类型函数,它返回一个描述给定类型实参大小(以字节为单位)的常量。另一方面,类模板也可以作为类型函数。类型函数的参数可以是模板的参数,而结果就是抽取出来的成员类型或成员变量。例如,可以把sizeof运算符改变成下面的接口:
// traits/sizeof.cpp #include <stddef.h>
#include <iostream> template <typename T>
class TypeSize
{
public:
static size_t const value = sizeof(T);
}; int main()
{
std::cout << "TypeSize<int>::value = " << TypeSize<int>::value << std::endl; // 抽取了int的成员value
}
接下来的内容,提供的是一些具有普遍用途的类型函数,而且它们都可以被用作trait类。
15.2.1 确定元素的类型
如下给定容器的类型,确定容器元素的类型:
// traits/elementtype.cpp #include <vector>
#include <list>
#include <stack>
#include <isotream>
#include <typeinfo> template <typename T>
class ElementT; // 基本模板 template <typename T>
class ElementT<std::vector<T> > // 局部特化
{
public:
typedef T Type;
}; template <typename T>
class ElementT<std::list<T> > // 局部特化
{
public:
typedef T Type;
}; template <typename T>
class ElementT<std::stack<T> > // 局部特化
{
public:
typedef T Type;
}; template <typename T>
void print_element_type (T const & c)
{
std::cout << "Container of " << typeid(typename ElementT<T>::Type).name() << "elements. \n";
} int main()
{
std::stack<bool> s;
print_element_type(s);
}
借助于局部特化的这种用法,即使在容器类型并没有意识到类型函数的情况下,也可以实现这种类型抽取。然而,大多数情况下,类型函数通常是和可应用类型(即这里的容器类型)一起实现的,而且这样的话,后面的设计通常都可以被简化。例如,如果容器类型定义了一个成员类型value_type(诸如标准容器的实现一样),那么我们就可以编写如下代码:
// 前面的三个特化可以简化成如下代码
template <typename C>
class ElementT
{
public:
typedef typename C::value_type Type;
};
上面的代码可以作为一种缺省实现,而且对于没有定义成员类型的value_type的容器类型,我们还可以进行特化,因为缺省实现和这里的特化是相容的。因此,我们通常建议在容器模板的定义内部,提供模板类型参数的类型定义,从而在泛型代码中可以更容易地访问这些参数类型,如下:
template <typename T1, typename T2, ..... >
class X
{
public:
typedef T1 ...;
typedef T2 ...;
....
};
类型函数之所以有用,是因为它使我们能够根据容器类型来参数化一个模板:从而在使用该模板的时候,我们并不需要给出代表元素类型和其他特征的一些参数。如下:
template <typename T, typename C>
T sum_of_elements(C const& c);
上面的代码要求我们使用诸如sum_of_elements<int>(list)的调用表达式(实参演绎,实参演绎不能用于返回值),也就是说需要显示指定元素的类型。然而,如果使用如下声明:
template <typename C>
typename ElementT<C>::Type sum_of_elements (C const& c);
那么我们就可以根据类型函数来抽取元素类型。
15.2.2 确定class 类型
运用下面的类型函数,我们能够确定某个函数是否为class类型:
// traits/isclasst.hpp template<typename T>
class IsClassT
{
private:
typedef char One;
typedef struct { char a[]; } Two;
template<typename C> static One test(int C::*); //更加特殊,接受 int类型的成员指针
template<typename C> static Two test(...); // 接受任何参数
public:
enum { Yes = sizeof(IsClassT<T>::test<T>()) == };
enum { No = !Yes};
};
上面的模板使用了SFINAE原则(substitution-failure-is-not-an-error,替换失败并非错误)。这里用到SFINAE原则的目的在于找到这样的一个类型构造:它对class类型是无效的,而对于其他的类型则是有效地;或者相反。于是,在这里我们可以依赖于下面这个事实:只有当C是一个class类型的时候,身为成员指针类型的类型构造C::*才会是有效的。
如下应用这个类型函数,来测试某个特定类型是否是class类型:
// triats/isclasst.cpp
#include <iostream>
#include "isclasst.hpp" class MyClass
{
}; struct MyStruct
{
}; union MyUnion
{
}; void myfunc()
{
} enum E{ e1 }e;
// 以模板实参的方式传递类型,并对该类型进行检查
template <typename T>
void check()
{
if (IsClassT<T>::Yes)
{
std::cout << "IsClassT " << std::endl;
}else
{
std::cout << " !IsClassT " << std::endl;
}
} // 以函数调用实参的方式传递类型,并对该类型进行检查
template <typename T>
void checkT(T)
{
check<T>();
} int main()
{
std::cout << " int: ";
check<int>(); std::cout << " MyClass: ";
check<MyClass>();
....
}
输出:
int: !IsClassT
MyClass: IsClassT
15.2.3 引用和限定符
考虑如下函数模板定义:
// traits/apply1.hpp template <typename T>
void apply(T& arg, void (*func)(T))
{
func(arg);
}
同时考虑下面使用代码:
// traits/apply1.cpp #include<iostream>
#include "apply1.hpp" void incr(int& a)
{
++a;
} void print(int a)
{
std::cout << a << std::endl;
} int main()
{
int x = ;
apply(x, print); // (1)
apply(x, incr); // (2)
}
(1)的调用正确:用int来替换T。
然而(2)的调用,如果匹配第2个参数,那么要求用int&来替换T,而这意味着第1个参数类型为int&&,但int&&通常都不是合法的C++类型。
借助局部特化,我们可以如下解决这个问题:
// traits/typeop1.hpp template <typename T>
class TypeOp // 基本模板
{
public:
typedef T ArgT;
typedef T BareT;
typedef T const ConstT; //添加const限定
typedef T & RefT;
typedef T & RefBareT;
typedef T const & RefConstT;
};
首先,我们可以实现一个处理const类型的局部特化
// traits/typeop2.hpp template <typename T>
class TypeOp<T const> // 针对const类型的局部特化
{
public:
typedef T const ArgT;
typedef T BareT; // 去除const限定
typedef T const ConstT;
typedef T const & RefT;
typedef T & RefBareT; // 添加&引用
typedef T const & RefConstT;
};
针对引用类型的局部特化同样也适用于reference-to-const类型:
// traits/typeop3.hpp template <typename T>
class TypeOp<T&> // 针对引用的局部特化
{
public:
typedef T & ArgT;
typedef typename TypeOp<T>::BareT BareT; // 引用的基本类型
typedef T const ConstT;
typedef T & RefT;
typedef typename TypeOp<T>::BareT & RefBareT;
typedef T const & RefConstT;
};
特殊情况:指向void的引用是不允许的:
// traits/typeop3.hpp template <>
class TypeOp<void> // 针对void的全局特化
{
public:
typedef void ArgT;
typedef void BareT;
typedef void const ConstT;
typedef void RefT;
typedef void RefBareT;
typedef void RefConstT;
};
有了上面这几部分代码,我们就可以改写apply模板如下:
// 现在只能根据第2个实参来演绎T了,因为T位于一个受限名称中
template <typename T>
void apply(typename TypeOp<T>::RefT arg, void (*func)(T))
{
func(arg);
}
15.2.4 promotion trait
我们已经研究并且开发了单一类型的类型函数:即给定一个类型,我们可以定义其他相关的类型或者参数。然而,我们通常都需要开发依赖于多个实参的类型参数。一个典型例子就是promotion trait,它在编写运算符模板的时候非常有用。
假设,我们需要对两个Array容器进行相加:
template <typename T>
Array<T> operator+ (Array<T> const&, Array<T> const &);
这看起来非常好。但是,由于语言允许我们把一个char类型的值加到一个int值,因此,我们期望可以对数组也实现这种混合类型的操作。于是,我们将面临一个问题,即如何确定结果模板的返回类型。
template <typename T1, typename T2>
Array<????> operator+ (Array<T1> const&, Array<T2> const &);
然而,借助于promotion trait,我们就可以解决上面声明所给出的问题。如下:
//method1
template<typename T1, typename T2>
Array<typename Promotion<T1, T2>::ResultT>
operator+ (Array<T1> const&, Array<T2> const&);
//或者另一种实现方法method2
template<typename T1, typename T2>
typename Promotion<Array<T1>, Array<T2> >::ResultT
operator+ (Array<T1> const&, Array<T2> const&);
上面的代码的主要的想法是:提供模板Promotion的一系列特化,从而能够根据要求生成一个满足我们需要的类型函数。
另一个使用promotion trait的应用程序是由max()模板引入的;当我们希望指定两个不同类型值的最大值时,我们通常都期望返回结果(即最大值)属于“两个类型中更加强大的类型”,而这个时候往往就会用到类型函数。
实际上,对于Promotion模板,并不存在确切的定义;因此,我们最好是让这个基本模板处于未定义状态:
template <typename T1, typename T2>
class Promotion;
另外,如果两个类型的大小不一样,那么我们还需要作出另一个选择:我们将提升类型更强大的类型。我们可以通过特殊模板IfThenElse来实现这一点,它会接受一个Boolean的非类型模板参数,然后根据Boolean参数的值,在两个类型参数之中选出其中一个:
// traits/ifthenelse.hpp
#ifndef IFTHENELSE_HPP
#define IFTHENELSE_HPP // 基本模板:根据第1个实参来决定:是选择第2个实参,还是第3个实参
template <bool C, typename Ta, typename Tb>
class IfThenElse; // 局部特化:true的话则选择第2个实参
template<typename Ta, typename Tb>
class IfThenElse<true, Ta, Tb>
{
public:
typedef Ta ResultT;
}; // 局部特化:false的话则选择第3个实参
template<typename Ta, typename Tb>
class IfThenElse<false, Ta, Tb>
{
public:
typedef Tb ResultT;
}; #endif // IFTHENELSE_HPP
有了上面的这些代码之后,我们能够根据所需要提升的类型的大小,从而在T1、T2、void三者之间做出选择,并且实现Promotion模板如下:
// traits/promote1.hpp // 针对类型提升(type promotion)的基本模板
template<typename T1, typename T2>
class Promotion
{
public:
typedef typename
IfThenElse<(sizeof(T1) > sizeof(T2),
T1,
typename IfThenElse<(sizeof(T1) < sizeof(T2)),
T2,
void
>::ResultT
>::ResultT ResultT;
};
对于在基本模板中使用的这种基于类型大小的启发式假设,在大多数情况下都可以正常运行;但我们需要对这种假设进行检验;而这有时候也是比较麻烦的。另外,如果这种假设选择了一个错误的(即不符合期望的)类型,那么我们还需要给出一个相应的特化,来改写原来这种(基于假设的)选择。另一方面,如果两个类型是完全一样的,那么马上就可以安全地把该(相同的)类型提升为所期望的类型。可以用下面的局部特化来阐述这一点:
// traits/promote2.hpp // 针对两个相同类型的局部特化
template<typename T>
class Promotion<T, T>
{
public:
typedef T ResultT;
};
为了记录基本类型的提升,我们还需要实现一系列针对基本类型的特化。在此,可以借助宏来(从某种程度地)减少源代码的数量:
// traits/promote3.hpp #define MK_PROMOTION(T1, T2, Tr) \
template<> class Promotion<T1, T2> { \
public: \
typedef Tr ResultT; \
}; \
\
template<> class Promotion<T2, T1> { \
public: \
typedef Tr ResultT; \
};
于是,我们可以这样添加这些提升:
// traits/promote4.hpp
MK_PROMOTION(bool, char, int)
MK_PROMOTION(bool, unsigned char, int)
MK_PROMOTION(bool, signed char, int)
.......
一旦为基本类型(和一些必要的枚举类型)定义好了Promotion,我们就可以通过局部特化来表达其他的提升规则。如Array数组:
// traits/promotearray.hpp
// 特化1
template<typename T1, typename T2>
class Promotion<Array<T1>, Array<T2> >
{
public:
typedef Array<typename Promotion<T1, T2>::ResultT> ResultT;
};
// 特化2
template<typename T, typename T>
class Promotion<Array<T>, Array<T> >
{
public:
typedef Array<typename Promotion<T, T>::ResultT> ResultT;
};
对于最后一个局部特化,我们需要给予更大的关注。我们刚开始可能会认为前面针对相同类型的特化(Promotion<T, T>)已经考虑了这种情况。然而遗憾的是,就特化程度而言,局部特化Promotion<Array<T1>, Array<T2> > 和局部特化Promotion<T, T>是一样的。为了避免产生这种(由于特化程度相同而引起的)模板选择二义性,我们添加了最后一个局部特化,它比前面两个模板中的任何一个都更加特殊化。
15.3 policy trait
到目前为止,我们给出了几个trait模板的例子,用于确定模板参数的一些属性:譬如这些参数表示的是什么类型;在混合类型的操作中,应该提升哪一个类型等等。我们把这些trait称为property trait。 另一方面,还存在其他类型的trait,它们定义了应该如何对待这些类型,我们把这类trait称为policy trait。尽管我们通常可以把property trait实现为类型函数,但是对于policy trait而言,我们通常是把该policy封装在成员函数内部。
15.3.1 只读的参数类型
在C和C++中,函数调用实参在缺省情况下都是以“传值”的方式进行传递的。但对于很大的数据结构应该采用“传const引用”。特别的,当引入模板之后,由于我们事先并不知道用来替换模板的参数类型究竟有多大;而且,最后的决定也不仅仅依赖于类型的大小:一个小的结构也可能会具有昂贵的拷贝构造函数。所以,选择一个适当的传递机制变得更加复杂。
在前面的讨论中,我们已经隐约提到,可以使用policy trait模板来处理上面这个问题,而且该policy trait实际上是一个类型函数:该函数可以根据不同的情况(即类型大小),将把实参类型T映射为T或者T const&,即在这两种类型中挑选一种最佳参数类型。基于下面的例子,我们做出一个近似的假设:对于不大于“2个指针”大小的类型,基本模板将采用“传值”的方式传递参数,而对于其他的类型,则采用“传递const引用”的方式传递参数。
template<typename T>
class RParam
{
public:
typedef typename IfThenElse<sizeof(T) <= *sizeof(void*),
T,
T const&>::ResultT Type;
};
另一方面,对于容器类型,即使sizeof函数返回的是一个很小的值,但也可能会涉及到昂贵的拷贝构造函数。因此,我们需要编写如下(针对Array)的许多特化和局部特化:
template<typename T>
class RParam<Array<T> >
{
public:
typedef Array<T> const& Type;
};
由于我们处理的都是C++中的常见类型,所以我们期望在基本模板中能够对非class类型以传值的方式进行调用。另外对于某些对性能要求比较苛刻的class类型,我们有选择地添加这些类为“传值”方式。
template<typename T>
class RParam
{
public:
// class类型,使用传const引用
typedef typename IfThenElse<IsClassT<T>::No,
T,
T const&>::ResultT Type;
}; // 特化 : 针对RParam<>的MyClass2参数,以传值的方式进行传递
template<>
class RParam<MyClass2>
{
public:
typedef MyClass2 Type;
};
15.3.2 拷贝、交换和移动
书中引入了一个policy trait模板,它将选择出最佳操作,来拷贝、交换或者移动某一特定类型的元素。