如果你问C++中traist是干嘛的,很多网友会给你扛出“吓人的解释”:特性萃取技术!提取“被传进的对象”对应的返回类型!……嗯,听起来很高大上很牛逼,but,我还是不明白这玩意有啥用?
相比枯燥的解释,让我们先看一个实际的需求。
用过STL容器的同学都知道,STL自己有一套内存管理,我们直接使用容器就行了,一般情况下,不用关心容器内部的内存处理。但总存在一些情况,我们必须自己手动管理一些对象的内存,比如一个比较大的项目中,有大量对象的频繁申请释放,需要设计一个内存池,让不同的对象都能使用这套内存池。在这个实际需求中,最基本的接口要有两个,一个是create,一个是destroy,它们的大致形式如下:
//负责申请一个T*数组
template<typename T>
T* create(int Count);
//负责销毁create申请的T*数组
template<typename T>
void destroy(T* ptr);
问题来了,函数体该如何实现?“这好办,最简单的,不就是new 和 delete吗?”OK,于是我们写出如下代码(为了简洁考虑,本文不对代码健壮性作处理):
//负责申请一个T*数组
template<typename T>
T* create(int Count)
{
return new T[Count];
}
//负责销毁create申请的T*数组
template<typename T>
void destroy(T* ptr)
{
delete[] T;
}
到这里,似乎没什么需要过多讨论的,但细想new和delete的做法,我们知道,new的步骤实际分为两步:第一步申请T的内存,第二步调用T的构造函数。delete正好相反,第一步调用析构函数,第二步归还T的内存。而new和malloc的区别、delete和free的区别,就在于是否处理构造函数、析构函数。malloc和free简单粗暴,只申请、销毁T的内存,不管构造函数、析构函数。
一般情况下,因为少干了两件事,所以malloc和free其实更高效,不过一般情况下我们感受不到区别,但在数量庞大的时候,malloc、free的效率便显示出来了,我们写下以下代码进行效率测试。
class A
{
public:
int a[100];
A() {}
~A() {}
};
//测试代码
const int TestCount = 10;
const int Cycle = 10 * 10000;
StartCountDurationTime(); //------start-------
for (int c = 0; c < Cycle; ++c)
{
A* a1 = new A[TestCount];
delete[] a1;
}
cout << GetDurationTime() << endl; //----end-----记录start到end的时间,单位ms
StartCountDurationTime();
for (int c = 0; c < Cycle; ++c)
{
A* a1 = (A*)malloc(sizeof(A)*TestCount);
memset(a1, 0, sizeof(A)*TestCount);
free(a1);
}
cout << GetDurationTime() << endl;
输出如下,具体时间可能因机器不同而异,但总的来说malloc的效率要比new高很多。
此时回头再看开始的create destroy代码,我们不禁发出思考:项目中create destroy的使用极为频繁,我们是否可以对像class A这种类作出优化,用malloc创建而不是new呢?因为对纯粹的结构A来说,对它调用构造函数是没有必要的。
有了这个疑问后,我们的create和destroy变成下面这样:
template<typename T>
T* create(int count)
{
//如何根据不同的T决定使用new 还是malloc?
}
template<typename T>
void destroy(T* ptr, int Count)
{
//如何根据不同的T决定使用delete[] 还是free?
}
进一步的问题其实是:我如何知道T需不需要调用构造函数?或者说,我如何知道调用T的构造函数是没有意义的?
如果有这么样一个模块,我传T给它,它返回一个玩意给我,我就用这个玩意来判断需不需要调用构造函数,大致代码如:
template<typename T>
T* create(int count)
{
if(Module<T>)
使用new
else
使用malloc
}
于是问题演变为:怎样设计一个模块,让它接受一个模板参数,返回一个别的、我能使用的数据?
如果我们对于模板编程不是很熟悉,似乎问题到此就难以解决了,先找相关模板书籍看看……
……
一段时间后
……
Good!通过相关的学习,我们知道了模板特化、typeid这些东西,类模板特化最大的作用就是通过给模板参数传不同的参数来生成不同的class,typeid可以用来判断两个类型是否相等。
于是我们的解决方案大致是:添加一个模板类,当参数是一些类时,我们返回类型A,当参数是另一些类时,我们返回类型B(这里用到了模板特化),然后根据模板返回的不同类型,决定使用new还是malloc。这个模板类起个什么名字呢?考虑到它的功能是类型转换,用convert不错,但咱们起个更牛逼的名字traits,显得更高大上!OK,代码大致如下:
struct my_true
{
};
struct my_false
{
};
template<typename T>
struct Traits_Need_New
{
using NeedNew = my_true;
};
template<>
struct Traits_Need_New<A>
{
using NeedNew = my_false;
};
template<typename T>
using Traist_t = typename Traits_Need_New<T>::NeedNew;
这里,我们添加了两个空类my_true和my_false,仅用于区分不同的类型。在模板类Traits_Need_New中,默认情况下,NeedNew被定义成my_true,如果模板实参是class A,那么NeedNew被定义成my_false。
使用代码如下:
template<typename T>
T* create(int count)
{
T* Ret = nullptr;
if (typeid(my_true) ==typeid(Traist_t<T>))
Ret = new T[count];
else
{
Ret = (T*)malloc(sizeof(T) * count);
memset(Ret, 0, sizeof(T) * count);
}
return Ret;
}
template<typename T>
void destroy(T* ptr, int Count)
{
if (typeid(my_true) ==typeid(Traist_t<T>))
delete[] ptr;
else
free(ptr);
}
这样一来,默认情况下,全部使用new和delete来处理内存,如果遇到了A,则
if (typeid(my_true) == typeid(Traits_Need_New<T>::NeedNew))判断失败,于是会使用malloc free。其他的类如果需要被优化,那么只要添加一个Traits_Need_New特化版本即可。
这个需求到此告一段落,回到文章开始的问题:traits是干嘛的?有什么用?
本文中的Traits_Need_New就是最简单的Traits技法,它的应用场合一般有如下特征:
- 目标函数或者类接受的参数是模板参数T,即具有统一形式,并且需要对T进行统一处理,如本文中需要对所有的T进行内存处理。
- 不同的实参T会导致不同的处理策略。如本文中class A不需要用new delete,而对于复杂的class就需要用new delete。而直接的T本身无法满足需求。
- 用一个中间模板类Traist对T和RetType进行胶合处理,这才是主要使用Traist的地方,也是Triast的字面意义:从一个给定的类型T,萃取出我们真正需要的特性,如本文中对T萃取出my_true和my_false,然后根据这个类型,进行分别的处理。
通过这个生动的例子,我们可以自信地说出来:Yes!traits是一种特性萃取技术(如果有别人这么问你,拿这句话去忽悠他,或者……直接告诉他本文地址☺)!