traits,又被叫做特性萃取技术,说得简单点就是提取“被传进的对象”对应的返回类型,让同一个接口实现对应的功能。因为STL的算法和容器是分离的,两者通过迭代器链接。算法的实现并不知道自己被传进来什么。萃取器相当于在接口和实现之间加一层封装,来隐藏一些细节并协助调用合适的方法,这需要一些技巧(例如,偏特化)。最后附带一个小小的例子,应该能更好地理解 特性萃取。
下面大部分来源于《STL源码剖析》,看原书能了解更多细节。
让我们一点点抛出问题,然后一点点深入。
1. 首先,在算法中运用迭代器时,很可能会用到其相应型别(迭代器所指之物的型别)。假设算法中有必要声明一个变量,以“迭代器所指对象的型别”为型别,该怎么办呢?
解决方法是:利用function template的参数推导机制。
template <class I, class T>
void func_impl(I iter, T t) {
T tmp; // 这里就是迭代器所指物的类型新建的对象
// … 功能实现
}
template
inline
void func(I iter) {
func_impl(iter, *iter); // 传入iter和iter所指的值,class自动推导
}
int main() {
int i;
func(&i);
}
这里已经可以看出封装的意思了,没有一层impl的封装的话,每次你都要显式地说明迭代器指向对象型别,才能新建tmp变量。加一层封装显得清爽很多。
迭代器相应型别不只是“迭代器所指对象的型别”一种而已。根据经验,最常用的相应型别有五种,然而并非任何情况下任何一种都可以利用上述的template参数推导机制来取得。
函数的“template参数推导机制”推导的只是参数,无法推导函数的返回值类型。万一需要推导函数的传回值,就无能为力了。
2. 声明内嵌型别似乎是个好主意,这样我们就可以直接获取。
template
struct MyIter {
typedef T value_type; // 内嵌型别声明
// ...
};
template
typename I::value_type
func(I ite) {
return *ite;
}
// …
MyIter
cout << func(ite);
看起来不错,但是并不是所有迭代器都是class type,原生指针就不行!如果不是class type,就无法为它定义内嵌型别。
这时候就需要 偏特化 出现。
3. 偏特化就是在特化的基础上再加一点限制,但它还是特化的template。
template
struct iterator_traits {
typedef typename I::value_type value_type;
};
template
struct iterator_traits<T*> {
typedef T value_type;
};
template
func(I ite) {
return *ite;
}
func在调用 I 的时候,首先把 I 传到萃取器中,然后萃取器就匹配最适合的 value_type。(萃取器会先匹配最特别的版本)这样当你传进一个原生指针的时候,首先匹配的是带<T*>的偏特化版本,这样 value_type 就是 T,而不是没有事先声明的 I::value_type。这样返回值就可以使用 typename iterator_traits::value_type 来知道返回类型。
https://www.cnblogs.com/mangoyuan/p/6446046.html
本质定义:加上一层间接性,换来以定的灵活性。
template
struct is_void
{ static const bool value = false; };
template <>
struct is_void
{ static const bool value = true; };
我们可以这样使用这份代码:
Is_void
Is_void
完整测试代码如下:
template
struct is_void
{
static const bool value = false;
};
template <>
struct is_void
{
static const bool value = true;
};
int _tmain(int argc, _TCHAR* argv[])
{
std::cout«is_void
std::cout<<is_void<void>::value;
return 0; }
https://docs.scala-lang.org/tour/traits.html
https://pypi.org/project/traits/4.6.0/
iterator模式定义如下:提供一种方法,使之能够依序寻访某个聚合物所含的各个元素,而又无需暴露该聚合物的内部表达形式
其中,c++里面各个容器的iterator扮演着将数据容器与算法结合起来的重要角色
将范型算法(find, count, find_if)用于某个容器中,最重要的是要给算法提供一个访问容器元素的工具,iterator就扮演着这个重要的角色
我们在算法中可能会定义简单的中间变量或者设定算法的返回变量类型,这时候需要知道迭代器所指元素的类型是什么,但是由于没有typeof这类判断类型的函数,我们无法直接获取,那该如何是好?
不要急,那首先先介绍一下iterator_tarit
iterator_trait
template
struct iterator_traits<_Tp*>
{
typedef ptrdiff_t difference_type;
typedef typename _Tp::value_type value_type;
typedef typename _Tp::pointer pointer;
typedef typename _Tp::reference reference;
typedef typename _Tp::iterator_category iterator_category;
};
看到这个奇奇怪怪的东西,是不是感觉没什么用,嗯,没关系,先记着
下面,将接着之前的话题,来看看如何提取出iterator所指向的元素类型
value_type
例如
使用typedef
我们可以在迭代器中添加元素的类型
template
struct MyIter {
typedef T value_type;
T * ptr;
MyIter(T * p = 0) : ptr (p) {};
T& operator* () const { return *ptr;}
};
template
typename I::value_type //取出迭代器类中的类型
//用以设定返回变量类型,但是如果I是指针就会错误
get (I ite) {
return *ite;
}
但是,这个版本并不支持原生指针,然而就迭代器的行为而言,就是面向容器的指针,而正常的STL算法也是支持原生指针的,就如同下面的find一样
指针和迭代器的作用无非就是为stl算法提供了一个运算范围以及对容器(无论是vector,list,亦或是array)的访问
int main() {
int a[5] = {1,2,2,2,2};
int *begin = a;
int *end = a+5;
int count = std::count(begin, end, 2); //ok!
return 0;
}
所以对于第一个版本,我们还要对指针类型进行模版偏特化
提取以及偏特化
前面也提到了,如果直接使用typename I::value_type,算法就无法接收原生指针,因为原生指针根本就没有value_type这个内嵌类型
因此,我们还需要加入一个中间层对其进行判断,看它是不是原生指针,注意,这就是traits技法的妙处所在
如果我们只使用上面的做法,也就是内嵌value_type,那么对于没有value_type的指针,我们只能对其进行偏特化,这种偏特化是针对可调用函数get的偏特化,假如get有100行代码,那么就会造成极大的视觉污染
#include
template
struct MyIter {
typedef T value_type;
T * ptr;
MyIter(T * p = 0) : ptr (p) {};
T& operator* () const { return *ptr;}
};
template
typename I::value_type //取出迭代器类中的类型
get (I ite) {
std::cout << "class version" << std::endl;
return *ite;
}
template
I get(I* ite) {
std::cout << "pointer version" << std::endl;
return *ite;
}
template
I get(const I* ite) {
std::cout << "const pointer version" << std::endl;
return *ite;
}
int main() {
int i = 3;
const int k = 3;
MyIter
std::cout << get(v) << std::endl;
std::cout << get(&i) << std::endl;
std::cout << get(&k) << std::endl;
return 0;
}
就如同上面这个形式,设想往get中填充100行代码,简直不忍直视,你再看看下面这个,简直优雅!
利用一个中间层iterator_traits固定了get的形式,使得重复的代码大量减少,唯一要做的就是稍稍特化一下iterator_tartis使其支持pointer和const pointer:)
#include
template
struct iterator_traits {
typedef typename T::value_type value_type;
};
template
struct iterator_traits<T*> {
typedef T value_type;
};
template
struct iterator_traits<const T*> {
typedef T value_type;
};
template
struct MyIter {
typedef T value_type;
T * ptr;
MyIter(T * p = 0) : ptr (p) {};
T& operator* () const { return *ptr;}
};
template
typename iterator_traits::value_type
get (I ite) {
std::cout << "normal version" << std::endl;
return *ite;
}
int main() {
int i = 3;
const int k = 3;
MyIter
std::cout << get(v) << std::endl;
std::cout << get(&i) << std::endl;
std::cout << get(&k) << std::endl;
return 0;
}
通过定义内嵌类型,我们获得了知晓iterator所指元素类型的方法,通过traits技法,我们将函数模板对于原生指针和自定义iterator的定义都统一起来
这就是traits技法的妙处所在
difference type
difference type用于表示两个迭代器之间的距离的一个类型,也可以用来表示一个容器的最大的容量,因为对于连续空间的容器,头尾之间的距离就是最大容量
例如count()就必须返回的类型就是迭代器的difference type
对于STL容器类型,以及原生指针,traits有如下两个不同版本
template
struct iterator_traits {
...
typedef typename I::difference_type difference_type;
}
//原生指针
template
struct iterator_traits<T*> {
...
typedef ptrdiff_t difference_type;
}
template
struct iterator_traits<const T*> {
...
typedef ptrdiff_t difference_type;
}
reference type
标示了引用类型
pointer
标示了指针类型
测试
以上说明了迭代器内部的几种重要类型
下面对其进行一个测试,以此产生一个更直观的印象
#include
#include
#define Test(x,z,y) std::cout«std::is_same<std::iterator_traits
int main() {
#define IVec std::vector
Test(IVec,value_type,int); //true
Test(IVec,difference_type,ptrdiff_t); //true
Test(IVec,reference,int&); //true
Test(IVec,pointer,int*); //true
return 0; } 从上面可以看出,一个vector<int>::iterator
value_type=int
difference_type=ptrdiff_t
reference=int&
pointer=int*
总结
要牢记iterator是为了访问容器内的元素而存在的,而它内置的类型就是范型算法与容器进行沟通的重要工具
而我们使用traits技法主要是为了解决原生指针和自定义iterator之间的不同所造成的代码冗余
type traits
type traits的出现和STL对于性能的要求有着千丝万缕的联系
试想,对于vector这种大块分配内存,然后大块析构的容器,如果容器里面是POD的话,那么只要等它的生命周期结束就行了,如果是非POD的话,那么就要判断是否拥有no-traits的析构函数
如果是这样的话,又回到了之前value_type的窘境,因此,我们只需要使用type_traits,对POD进行偏特化,通过两个神奇的类型进行判断
struct _true_type{};//无意义的析构函数
struct _false_type{};//有意义的析构函数
这样子就可以让负责析构的模块进行判断了
具体的type_traits如下所示
template
struct type_traits
{
typedef _false_type has_trivial_default_constructor;//默认构造函数是否有意义?
typedef _false_type has_trivial_copy_constructor;//拷贝构造函数是否有意义?
typedef _false_type has_trivial_assgignment_constructor;//拷贝赋值操作是否有意义?
typedef _false_type has_trivial_destructor;//析构函数是否有意义?
/*POD意指Plain Old Data,也就是标量型别或传统的C struct(传统的C struct只能
包含数据成员,不能包含函数成员。也就是所谓的聚合类。POD型别必然包含无意义
的ctor/dtor/copy/assignment函数。
*/
typedef _false_type is_POD_type;//是否为Plain Old Data? }; 总结 通过对type_traits进行特化,标注自己类中的构造,拷贝等行为是否是有意义的,可以大大提高适配算法的效率,这也是type traits存在的意义
template
typename T::innerClass myInnerObject;
这里的 typename 告诉编译器,T::innerClass 是一个类,程序要声明一个 T::innerClass 类的对象,而不是声明 T 的静态成员,而 typename 如果换成 class 则语法错误。
traits -》特性萃取机
通过function template 参数推导机制,可以是实现迭代器类型的推导,然后如果需要推导返回值类型,那么参数推导机制就不可行了,这时候通过内嵌类型声明推导返回值类型。所以出现了traits机制。
STL将容器和算法分离,算法实现的过程中并不知道进来的迭代器是哪个容器的,所以加入一层traits封装,traits会榨取所有进入算法的迭代器的类型。
所以说traits就是在接口和实现中间的一层封装。
我们可以继续 针对 template 参数更进一步,所谓偏特化便提了出来,偏特化就是针对template参数更进一步的条件限制所设计出来的特化版本。
template
class C {...}; //这个泛化版本支持T为任何类型
template
class C<T*> {...}; //这个泛化版本仅适用于"T为原生指针的情况"
//T为原生指针便是T为任何类型的进一步条件限制
参考 STL 源码剖析这本书,实现如下代码,对traits 机制有了更深的理解。(其实就是多了一层封装,得到迭代器所有类型)
https://blog.csdn.net/weixin_42014622/article/details/81294978
Traits不是一种语法特性,而是一种模板编程技巧。Traits在C++标准库,尤其是STL中,有着不可替代的作用。
如何在编译期间区分类型
下面我们看一个实例,有四个类,Farm、Worker、Teacher和Doctor,我们需要区分他们是脑力劳动者还是体力劳动者。以便于做出不同的行动。
这里的问题在于,我们需要为两种类型提供一个统一的接口,但是对于不同的类型,必须做出不同的实现。
我们不希望写两个函数,然后让用户去区分。
于是我们借助了函数重载,在每个类的内部内置一个work_type,然后根据每个类的word_type,借助强大的函数重载机制,实现了编译期的类型区分,也就是编译期多态。
代码如下:
复制代码
#include
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
void __distinction(const T &t, brain_worker)
{
cout << "脑力劳动者" << endl;
}
template
void __distinction(const T &t, physical_worker)
{
cout << "体力劳动者" << endl;
}
template
void distinction(const T &t)
{
typename T::worker_type _type; //为了实现重载
__distinction(t, _type);
}
int main(int argc, char const *argv[])
{
Worker w;
distinction(w);
Farmer f;
distinction(f);
Teacher t;
distinction(t);
Doctor d;
distinction(d);
return 0;
}
复制代码
在distinction函数中,我们先从类型中提取出worker_type,然后根据它的类型,选取不同的实现。
问题来了,如果不在类中内置worker_type,或者有的类已经写好了,无法更改了,那么怎么办?
使用Traits
我们的解决方案是,借助一种叫做traits的技巧。
我们写一个模板类,但是不提供任何实现:
//类型traits
template
class TypeTraits;
然后我们为每个类型提供一个模板特化:
复制代码
//为每个类型提供一个特化版本
template <>
class TypeTraits
{
public:
typedef physical_worker worker_type;
};
template <>
class TypeTraits
{
public:
typedef physical_worker worker_type;
};
template <>
class TypeTraits
{
public:
typedef brain_worker worker_type;
};
template <>
class TypeTraits
{
public:
typedef brain_worker worker_type;
};
复制代码
然后在distinction函数中,不再是直接寻找内置类型,而是通过traits抽取出来。
复制代码
template
void distinction(const T &t)
{
//typename T::worker_type _type;
typename TypeTraits
__distinction(t, _type);
}
复制代码
上面两种方式的本质区别在于,第一种是在class的内部内置type,第二种则是在类的外部,使用模板特化,class本身对于type并不知情。
两种方式结合
上面我们实现了目的,类中没有work_type时,也可以正常运行,但是模板特化相对于内置类型,还是麻烦了一些。
于是,我们仍然使用内置类型,也仍然使用traits抽取work_type,方法就是为TypeTraits提供一个默认实现,默认去使用内置类型,把二者结合起来。
这样我们去使用TypeTraits
复制代码
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
class TypeTraits
{
public:
typedef typename T::worker_type worker_type;
};
复制代码
OK,我们现在想添加一个新的class,于是我们有两种选择,
一是在class的内部内置work_type,通过traits的默认实现去抽取type。
一种是不内置work_type,而是通过模板的特化,提供work_type。
例如:
复制代码
class Staff
{
};
template <>
class TypeTraits
{
public:
typedef brain_worker worker_type;
};
复制代码
测试仍然正常:
Staff s;
distinction(s);
进一步简化
这里我们考虑的是内置的情形。对于那些要内置type的类,如果type个数过多,程序编写就容易出现问题,我们考虑使用继承,先定义一个base类:
template
struct type_base
{
typedef T worker_type;
};
所有的类型,通过public继承这个类即可:
复制代码
class Worker : public type_base
{
};
class Farmer : public type_base
{
};
class Teacher : public type_base
{
};
class Doctor : public type_base
{
};
复制代码
看到这里,我们应该明白,traits相对于简单内置类型的做法,强大之处在于:如果一个类型无法内置type,那么就可以借助函数特化,从而借助于traits。而内置类型仅仅使用于class类型。
以STL中的迭代器为例,很多情况下我们需要辨别迭代器的类型,
例如distance函数计算两个迭代器的距离,有的迭代器具有随机访问能力,如vector,有的则不能,如list,我们计算两个迭代器的距离,就需要先判断迭代器能否相减,因为只有具备随机访问能力的迭代器才具有这个能力。
我们可以使用内置类型来解决。
可是,许多迭代器是使用指针实现的,指针不是class,无法内置类型,于是,STL采用了traits来辨别迭代器的类型。
最后,我们应该认识到,traits的基石是模板特化。
STL中,traits编程技法得到了很大的应用,了解这个,才能一窥STL奥妙所在。
先将自己所理解的记录如下:
Traits技术可以用来获得一个 类型 的相关信息的。 首先假如有以下一个泛型的迭代器类,其中类型参数 T 为迭代器所指向的类型:
template
class myIterator
{
...
};
当我们使用myIterator时,怎样才能获知它所指向的元素的类型呢?我们可以为这个类加入一个内嵌类型,像这样:
template
class myIterator
{
typedef T value_type;
...
};
这样当我们使用myIterator类型时,可以通过 myIterator::value_type来获得相应的myIterator所指向的类型。
现在我们来设计一个算法,使用这个信息。
template
typename myIterator
{
...
}
这里我们定义了一个函数Foo,它的返回为为 参数i 所指向的类型,也就是T,那么我们为什么还要兴师动众的使用那个value_type呢? 那是因为,当我们希望修改Foo函数,使它能够适应所有类型的迭代器时,我们可以这样写:
template
typename I::value_type Foo(I i)
{
...
}
现在,任意定义了 value_type内嵌类型的迭代器都可以做为Foo的参数了,并且Foo的返回值的类型将与相应迭代器所指的元素的类型一致。至此一切问题似乎都已解决,我们并没有使用任何特殊的技术。然而当考虑到以下情况时,新的问题便显现出来了:
原 生指针也完全可以做为迭代器来使用,然而我们显然没有办法为原生指针添加一个value_type的内嵌类型,如此一来我们的Foo()函数就不能适用原 生指针了,这不能不说是一大缺憾。那么有什么办法可以解决这个问题呢? 此时便是我们的主角:类型信息榨取机 Traits 登场的时候了
….drum roll……
我们可以不直接使用myIterator的value_type,而是通过另一个类来把这个信息提取出来:
template
class Traits
{
typedef typename T::value_type value_type;
};
这样,我们可以通过 Traits
template
typename Traits::value_type Foo(I i)
{
...
}
然而,即使这样,那个原生指针的问题仍然没有解决,因为Trait类一样没办法获得原生指针的相关信息。于是我们祭出C++的又一件利器--偏特化(partial specialization):
template
class Traits<T*> //注意 这里针对原生指针进行了偏特化
{
typedef typename T value_type;
};
通过上面这个 Traits的偏特化版本,我们陈述了这样一个事实:一个 T* 类型的指针所指向的元素的类型为 T。
如此一来,我们的 Foo函数就完全可以适用于原生指针了。比如:
int * p;
….
int i = Foo(p);
Traits会自动推导出 p 所指元素的类型为 int,从而Foo正确返回。
过程:内嵌型别->traite类->模板偏特化=>可萃取原生指针的value type。
traits,又被叫做特性萃取技术,说得简单点就是提取“被传进的对象”对应的返回类型,让同一个接口实现对应的功能。因为STL的算法和容器是分离的,两者通过迭代器链接。算法的实现并不知道自己被传进来什么。萃取器相当于在接口和实现之间加一层封装,来隐藏一些细节并协助调用合适的方法,这需要一些技巧(例如,偏特化)。最后附带一个小小的例子,应该能更好地理解 特性萃取。
下面大部分来源于《STL源码剖析》,看原书能了解更多细节。
Traits编程技法
让我们一点点抛出问题,然后一点点深入。
1. 首先,在算法中运用迭代器时,很可能会用到其相应型别(迭代器所指之物的型别)。假设算法中有必要声明一个变量,以“迭代器所指对象的型别”为型别,该怎么办呢?
解决方法是:利用function template的参数推导机制。
复制代码
1 template <class I, class T>
2 void func_impl(I iter, T t) {
3 T tmp; // 这里就是迭代器所指物的类型新建的对象
4 // … 功能实现
5 }
6
7 template
8 inline
9 void func(I iter) {
10 func_impl(iter, *iter); // 传入iter和iter所指的值,class自动推导
11 }
12
13 int main() {
14 int i;
15 func(&i);
16 }
复制代码
这里已经可以看出封装的意思了,没有一层impl的封装的话,每次你都要显式地说明迭代器指向对象型别,才能新建tmp变量。加一层封装显得清爽很多。
迭代器相应型别不只是“迭代器所指对象的型别”一种而已。根据经验,最常用的相应型别有五种,然而并非任何情况下任何一种都可以利用上述的template参数推导机制来取得。
函数的“template参数推导机制”推导的只是参数,无法推导函数的返回值类型。万一需要推导函数的传回值,就无能为力了。
2. 声明内嵌型别似乎是个好主意,这样我们就可以直接获取。
复制代码
1 template
2 struct MyIter {
3 typedef T value_type; // 内嵌型别声明
4 // ...
5 };
6
7 template
8 typename I::value_type
9 func(I ite) {
10 return *ite;
11 }
12
13 // ...
14 MyIter
15 cout << func(ite);
复制代码
看起来不错,但是并不是所有迭代器都是class type,原生指针就不行!如果不是class type,就无法为它定义内嵌型别。
这时候就需要 偏特化 出现。
3. 偏特化就是在特化的基础上再加一点限制,但它还是特化的template。
复制代码
1 template
2 struct iterator_traits {
3 typedef typename I::value_type value_type;
4 };
5
6 template
7 struct iterator_traits<T*> {
8 typedef T value_type;
9 };
10
11 template
13 func(I ite) {
14 return *ite;
15 }
复制代码
func在调用 I 的时候,首先把 I 传到萃取器中,然后萃取器就匹配最适合的 value_type。(萃取器会先匹配最特别的版本)这样当你传进一个原生指针的时候,首先匹配的是带<T*>的偏特化版本,这样 value_type 就是 T,而不是没有事先声明的 I::value_type。这样返回值就可以使用 typename iterator_traits::value_type 来知道返回类型。
https://www.cnblogs.com/mangoyuan/p/6446046.html
介绍traits的文章很多,但感觉大部分文章的说明都很晦涩难懂,把一个并不很复杂的C++模板的应用描述的过于复杂。忍不住想把自己的理解跟大家分享一下,或许我也只是掌握了一点traits的皮毛而已,但也希望这些皮毛能略微抓住你的眼球,带给你一些启发。
首先,介绍traits前,回味一下C++的模板及应用,如果你脑海里浮现出的只是为实现一些函数或类的重用的简单模板应用,那我要告诉你,你out了。最近在整理一些模板的应用方式,有时间的话会写出来分享给大家,本文不会去详细讨论traits以外的模板的各种高级应用。
那么,言归正传,什么是traits?其实它并不是一个新的概念,上个世纪90年代中期就已经被提出,只是到了这个世纪才在各个C++库中被广泛使用,而我也是在这个概念诞生十多年后才接触到它。
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”.
我不知道官方或一些书上是如何去解释traits的,我的理解是:
当函数,类或者一些封装的通用算法中的某些部分会因为数据类型不同而导致处理或逻辑不同(而我们又不希望因为数据类型的差异而修改算法本身的封装时),traits会是一种很好的解决方案。
本以为能很简单的描述它,谁知道还是用了如此长的句子才说明清楚,相当的惭愧。大家只要有个大概的概念就ok了,甚至即使完全没概念也没关系,下面会通过实际代码来说明。
先看这样一个例子。如果有一个模板类Test:
template
class Test {
......
};
假设有这样的需求,类Test中的某部分处理会随着类型T的不同而会有所不同,比如希望判断T是否为指针类型,当T为指针类型时的处理有别于非指针类型,怎么做?
模板里再加个参数,如下:
template <typename T, bool isPointer>
class Test {
……// can use isPointer to judge whether T is a pointer
};
然后用户通过多传一个模板类型来告诉Test类当前T是否为指针。(Test<int*, true>)
很抱歉,所有的正常点的用户都会抱怨这样的封装,因为用户不理解为什么要让他们去关心自己的模板类型是否为指针,既然是Test类本身的逻辑,为什么麻烦用户呢?
由于我们很难去限制用户在使用模板类时是使用指针还是基本数据类型还是自定义类型,而用常规方法也没有很好的方法去判断当前的T的类型。traits怎么做呢?
定义traits结构:
template
struct TraitsHelper {
static const bool isPointer = false;
};
template
struct TraitsHelper<T *> {
static const bool isPointer = true;
};
也许你会很困惑,结构体里就一个静态常量,没有任何方法和成员变量,有什么用呢?解释一下,第一个结构体的功能是定义所有TraitsHelper中isPointer的默认值都是false,而第二个结构体的功能是当模板类型T为指针时,isPointer的值为true。也就是说我们可以如下来判断当前类型:
TraitsHelper
TraitsHelper<int*>::isPointer值为true, 可以得出当前类型int*为指针类型
也许看到这里部分人会认为我简直是在说废话,请再自己品味下,这样是否就可以在上面Test类的定义中直接使用TraitsHelper
if (TraitsHelper
......
else
......
再看第二个例子:
还是一个模板类Test:
template
class Test {
public:
int Compute(int d);
private:
T mData;
};
它有一个Compute方法来做一些计算,具有int型的参数并返回int型的值。
现在需求变了,需要在T为int类型时,Compute方法的参数为int,返回类型也为int,当T为float时,Compute方法的参数为float,返回类型为int,而当T为其他类型,Compute方法的参数为T,返回类型也为T,怎么做呢?还是用traits的方式思考下。
template
struct TraitsHelper {
typedef T ret_type;
typedef T par_type;
};
template <>
struct TraitsHelper
typedef int ret_type;
typedef int par_type;
};
template <>
struct TraitsHelper
typedef float ret_type;
typedef int par_type;
};
然后我们再把Test类也更新下:
template
class Test {
public:
TraitsHelper
private:
T mData;
};
可见,我们把因类型不同而引起的变化隔离在了Test类以外,对用户而言完全不需要去关心这些逻辑,他们甚至不需要知道我们是否使用了traits来解决了这个问题。
到这里,再让我们回过来取品味下开始我说的那句话:
当函数,类或者一些封装的通用算法中的某些部分会因为数据类型不同而导致处理或逻辑不同时,traits会是一种很好的解决方案。