本文主要介绍一下stl中经常用到的traits技巧。
1. C++ traits技术
关于traits技术,官方也没有一个很明确的理论描述,也有点晦涩。我们从网上找到几个关于traits的描述:
c++之父Bjarne Stroustrup关于traits的描述
Think of a trait as a small object whose main purpose is to carry information used by another object or algorithm to determine “policy” or “implementation details”.
简单翻译一下: trait是一个小对象,其主要目标是将另一个对象或算法的相关信息提取出来,用于确定策略
或实现细节
Traits both provide a set of methods that implement behaviour to a class, and require that the class implement a set of methods that parameterize the provided behaviour.
简单翻译一下: traits提供了一系列的方法用于实现一个类的行为,同时也要求该类对traits所指定的行为提供参数化实现。
综合上面,我认为可以这样理解: traits是对某一类特征
或行为
的抽象,同时要求在编译时(compile-time)就能够完成抽象的具体化。我们知道abstract class也可以通过继承的方式实现类似的功能,但abstract class是在运行时完成抽象的具体化的。如下图所示:
上图所示,无论是traits还是abstract class中均有两种对象,其中traits是通过萃取的方式来提取共同特征或行为的,而abstract是通过继承的方式来获得共同的特征或行为。可以看到,traits具有比abstract class更松散的耦合性。
traits不是一种语法特性,而是一种模板编程技巧。Traits在C++标准库,尤其是STL中,有着不可替代的作用。
2. 如何在编译期间区分类型
下面我们看一个实例,有四个类:Farm、Worker、Teacher和Doctor,我们需要区分他们是脑力劳动者还是体力劳动者,以便做出不同的行动。
这里的问题在于,我们需要为两种类型提供一个统一的接口,但是对于不同的类型,必须做出不同的实现 。我们不希望写两个函数,然后让用户去区分。于是我们借助了函数重载,在每个类的内部内置一个work_type,然后根据每个类的work_type,借助强大的函数重载机制,实现了编译期的类型区分,也就是编译期多态。
代码如下:
#include <iostream>
using namespace std;
//两个标签类
struct brain_worker{}; //脑力劳动
struct physical_worker{}; //体力劳动
class Worker{
public:
typedef physical_worker worker_type;
};
class Farmer{
public:
typedef physical_worker worker_type;
};
class Teacher{
public:
typedef brain_worker worker_type;
};
class Doctor{
public:
typedef brain_worker worker_type;
};
template<typename T>
void __distinction(const T &t, brain_worker){
cout<<"脑力劳动者"<<endl;
}
template<typename T>
void __distinction(const T &t, physical_worker){
cout<<"体力劳动者"<<endl;
}
template<typename T>
void distinction(const T &t){
typename T::worker_type _type; //为了实现重载
__distinction(t, _type);
}
int main(int argc, char *argv[]){
Worker w;
Farmer f;
Teacher t;
Doctor d;
distinction(w);
distinction(f);
distinction(t);
distinction(d);
return 0x0;
}
编译运行:
# gcc -o test test.cpp -lstdc++
# ./test
体力劳动者
体力劳动者
脑力劳动者
脑力劳动者
在distinction函数中,我们先从类型中提取出worker_type,然后根据它的类型,选取不同的实现。问题来了,如果不在类中内置worker_type,无法更改了,那么怎么办?
2.1 使用traits
我们的解决方案是,借助一种叫traits的技巧。
我们写一个模板类,但是不提供任何实现:
//类型traits
template <typename T>
class TypeTraits;
然后,我们为每一个类型提供一个模板特化:
//为每个类型提供一个特化版本
template<>
class TypeTraits<Worker>{
public:
typedef physical_worker worker_type;
};
template<>
class TypeTraits<Farmer>{
public:
typedef physical_worker worker_type;
};
template<>
class TypeTraits<Teacher>{
public:
typedef brain_worker worker_type;
};
template<>
class TypeTraits<Doctor>{
public:
typedef brain_worker worker_type;
};
然后在distinction函数中,不再是直接寻找内置类型,而是通过traits抽取出来:
template<typename T>
void distinction(const T &t){
typename TypeTraits<T>::worker_type _type;
__distinction(t, _type);
}
上面两种方式的本质区别在于: 第一种是在class的内部内置type,第二种则是在类的外部,使用模板特化,class本身对于type并不知情。
下面给出完整源码:
#include <iostream>
using namespace std;
//两个标签类
struct brain_worker{}; //脑力劳动
struct physical_worker{}; //体力劳动
class Worker{};
class Farmer{};
class Teacher{};
class Doctor{};
template <typename T>
class TypeTraits;
//为每个类型提供一个特化版本
template<>
class TypeTraits<Worker>{
public:
typedef physical_worker worker_type;
};
template<>
class TypeTraits<Farmer>{
public:
typedef physical_worker worker_type;
};
template<>
class TypeTraits<Teacher>{
public:
typedef brain_worker worker_type;
};
template<>
class TypeTraits<Doctor>{
public:
typedef brain_worker worker_type;
};
template<typename T>
void __distinction(const T &t, brain_worker){
cout<<"脑力劳动者"<<endl;
}
template<typename T>
void __distinction(const T &t, physical_worker){
cout<<"体力劳动者"<<endl;
}
template<typename T>
void distinction(const T &t){
typename TypeTraits<T>::worker_type _type;
__distinction(t, _type);
}
int main(int argc, char *argv[]){
Worker w;
Farmer f;
Teacher t;
Doctor d;
distinction(w);
distinction(f);
distinction(t);
distinction(d);
return 0x0;
}
编译运行:
# gcc -o test test.cpp -lstdc++
# ./test
体力劳动者
体力劳动者
脑力劳动者
脑力劳动者
2.2 两种方式结合
上面我们实现了目的,类中没有worker_type时,也可以正常运行。但是模板特化相对于内置类型,还是麻烦了一些。
于是我们仍然使用内置类型,也仍然使用traits抽取worker_type,方法就是为TypeTraits提供一个默认实现,默认去使用内置类型,把二者结合起来。这样我们去使用TypeTraits::worker_type时,有内置类型的就使用默认实现,无内置类型的就需要提供特化版本。
class Worker{
public:
typedef physical_worker worker_type;
};
class Farmer{
public:
typedef physical_worker worker_type;
};
class Teacher{
public:
typedef brain_worker worker_type;
};
class Doctor{
public:
typedef brain_worker worker_type;
};
//类型traits
template<typename T>
class TypeTraits{
public:
typedef typename T::worker_type worker_type;
};
OK,我们现在想添加一个新的class,于是我们有两种选择:
例如:
class Staff{};
template<>
class TypeTraits<Staff>{
public:
typedef brain_worker worker_type;
};
测试仍然正常:
Staff s;
distinction(s);
2.3 进一步简化
这里我们考虑的是内置的情形,对于那些要内置type的类,如果type个数过多,程序编写就容易出现问题,我们考虑使用继承,先定义一个base类:
template <typename T>
struct type_base{
typedef T worker_type;
};
所有的类型,通过public继承这个类即可:
class Worker : public type_base<physical_worker>{
};
class Farmer : public type_base<physical_worker>{
};
class Teacher : public type_base<brain_worker>{
};
class Doctor : public type_base<brain_worker>{
};
看到这里我们应该明白,traits相对于简单内置类型的做法,强大之处在于: 如果一个类型无法内置type,那么就可以借助函数特化,从而借助于traits。而内置类型仅仅适用于class类型。
以STL中的迭代器为例,很多情况下我们需要辨别迭代器的类型。例如distance函数计算两个迭代器的距离,有的迭代器具有随机访问能力,如vector,有的则不能,如list。我们计算两个迭代器的距离,就需要先判断迭代器能否相减,因为只有具备随机访问能力的迭代器才具有这个能力。
我们可以使用内置类型来解决。可是,许多迭代器是使用指针实现的,指针不是class,无法内置类型,于是STL采用traits来辨别迭代器的类型。
最后,我们应该认识到,traits的基石是模板特化 。
3. value traits
我们在上文介绍了type traits,下面我们简要介绍一下value traits。我们来看一下累加函数:
template <typename T>
struct traits;
template<>
struct traits<char>{
typedef int AccuT;
};
template<>
struct traits<int>{
typedef int AccuT;
};
template <class T>
typename traits<T>::AccuT accum3(const T* ptr, int len){
traits<T>::AccuT total = traits<T>::AccuT();
for(int i = 0; i<len; i++){
total += *(ptr + i);
}
return total;
}
注意如下这行代码:
traits<T>::AccuT total = traits<T>::AccuT();
如果AccuT是int、float等类型,那么int()、float()就会初始化成0,没有问题,那么万一对应的类型不可以()初始化呢?
这个时候就用到了value traits,可以把traits改写成如下:
template<>
struct traits<int>{
typedef int AccuT;
static AccuT const Zero = 0;
};
这个traits里面有一个类型和一个值。然后把累加函数改成:
template<class T>
typename traits<T>::AccuT accum3(const T* ptr, int len){
typename traits<T>::AccuT total = traits<T>::Zero;
for(int i = 0; i < len; i++){
total += *(ptr + i);
}
return total;
};
这样就可以解决初始化问题了。然后就算变动,也只需要修改traits里面的Zero了。
但是这么做也有个问题,就是并不是所有的类型都可以在类里面初始化。比如,我们把traits的返回值类型改成double:
template<>
struct traits<int>{
typedef double AccuT;
static AccuT const Zero = 0;
};
这样编译器直接报错(vs2012):
error C2864: ‘traits::Zero' : only static const integral data members can be initialized within a class
有些人会在类外面来初始化这个值,比如: double const traits::Zero = 0;这也是个办法。但是感觉这不是个好办法。更一般的是在traits内部搞个静态函数,然后在累加函数里面调用函数而不是静态变量。代码:
#include <iostream>
template<typename T>
struct traits;
template<>
struct traits<char>
{
typedef int AccuT;
static AccuT Zero(){return 0;}
};
template<>
struct traits<int>
{
typedef double AccuT;
static AccuT Zero(){ return 0.0; };
};
template<class T>
typename traits<T>::AccuT accum3(const T* ptr, int len)
{
typename traits<T>::AccuT total = traits<T>::Zero();
for (int i = 0; i < len; i++)
{
total += *(ptr + i);
}
return total;
}
int main(int argc, char* argv[])
{
int sz[] = {1, 2, 3};
traits<int>::AccuT avg = accum3(sz, 3) / 3;
std::cout<<"avg: "<<avg<<std::endl;
char ch[] = {'a', 'b', 'c'};
traits<char>::AccuT avg2 = accum3(ch, 3)/3;
std::cout<<"avg: "<<char(avg2)<<std::endl;
return 0;
}
编译运行:
# gcc -o test test.cpp -lstdc++
# ./test
avg: 2
avg: b
4. policy 与 traits
一讲到traits,相应的就会联系到policy。那么policy是干啥的呢?看一下下面的累加代码:
template <class T>
typename traits<T>::AccuT accum(T *ptr, int len){
typename traits<T>::AccuT total = traits<T>::Zero();
for(int i = 0; i < len; i++){
total += *(ptr + i);
}
return total;
}
注意total += *(ptr+i);
这一行,这里有两个问题:
想解决这2个问题,首先想到的应该就是这个地方不要hard code了,通过传进来参数的方法来动态修改这行代码。一般有两种方法:
这里我们介绍第二种方法: policy。我们把累加函数改成:
template<class T, class P>
typename traits<T>::AccuT accum(T *ptr, int len){
typename traits<T>::AccuT total = traits<T>::Zero();
for(int i = 0; i < len; i++){
P::accumulate(total, *(ptr+i));
}
return total;
}
注意,我们多加了一个模板参数P,然后调用P::accumulate()。现在我们来定义这个P类:
class P{
public:
template<class T1, class T2>
static void accumulate(T1 &total, T2 v){
total += v;
}
};
P类里面只有一个函数,是个静态模板函数。这样我们就可以调用了:
int sz[] = {1,2,3};
traits<int>::AccuT avg = accum<int, P>(sz,3)/3;
上面我们得到结果为2。
那如果我现在要传入一个字符数组,并且想逆序拼接,应该怎么做呢?
1) 首先把traits的返回类型改为string
template<>
struct traits<char>{
typedef std::string AccuT;
static AccuT Zero(){return std::string("");}
};
然后,新增一个policy类:
class P2{
public:
template<class T1, class T2>
void accumulate(T1 &total, T2 v){
total.insert(0,1,v);
}
};
调用一下,就会发现返回值是cba
了:
char str[] = {'a', 'b', 'c'};
traits<char>::AccuT ret = accum<char,P2>(str, 3);
如果调用accum(char, P>(str, 3);就会返回abc
,因为string本身也支持+=
,所有P::accumulate()可以正常运行,并且顺序拼接。
[参看]:
模板:什么是Traits
C++模板 - value traits
traits and policy
stl源代码