您的位置:首页 > 编程语言 > C语言/C++

【C++模版之旅】神奇的Traits

2016-05-05 01:22 573 查看
【转自:

/article/1411820.html

/article/1411810.html



一、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 <typename T>
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 <typename T>
struct TraitsHelper {
static const bool isPointer = false;
};
template <typename T>
struct TraitsHelper<T*> {
static const bool isPointer = true;
};


也许你会很困惑,结构体里就一个静态常量,没有任何方法和成员变量,有什么用呢?

解释一下,第一个结构体的功能是定义所有TraitsHelper中isPointer的默认值都是false,而第二个结构体的功能是当模板类型T为指针时,isPointer的值为true。也就是说我们可以如下来判断当前类型:

TraitsHelper< int>::isPointer值为false, 可以得出当前类型int非指针类型

TraitsHelper< int* >::isPointer值为true, 可以得出当前类型int*为指针类型

也许看到这里部分人会认为我简直是在说废话,请再自己品味下,这样是否就可以在上面Test类的定义中直接使用TraitsHelper::isPointer来判断当前T的类型了。

if (TraitsHelper<T>::isPointer)
......
else
......


再看第二个例子:

还是一个模板类Test:

template <typename T>
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 <typename T>
struct TraitsHelper {
typedef T ret_type;
typedef T par_type;
};
template <>
struct TraitsHelper<int> {
typedef int ret_type;
typedef int par_type;
};
template <>
struct TraitsHelper<float> {
typedef float ret_type;
typedef int par_type;
};


然后我们再把Test类也更新下:

template <typename T>
class Test {
public:
TraitsHelper<T>::ret_type Compute(TraitsHelper<T>::par_type d);
private:
T mData;
};


可见,我们把因类型不同而引起的变化隔离在了Test类以外,对用户而言完全不需要去关心这些逻辑,他们甚至不需要知道我们是否使用了traits来解决了这个问题。

到这里,再让我们回过来取品味下开始我说的那句话:

当函数,类或者一些封装的通用算法中的某些部分会因为数据类型不同而导致处理或逻辑不同时,traits会是一种很好的解决方案。

三、项目经历

先描述下问题,项目中有这样一个接口类会暴露给外部使用,接口类定义如下(类方法名称以及描述该问题无关的内容会有所修改、省略或删除):

class IContainer
{
public:
virtual RESULT Insert(const std::string& key, const ExportData& data) = 0;
virtual RESULT Delete(const std::string& key) = 0;
virtual RESULT Find(const std::string& key, ExportData& data) = 0;
};


从内容和名称很容易看出该接口不外乎一个容器,可以进行增、改、查操作,太简单了,这样的接口类会有什么样的问题呢?

从接口类的方法中可以发现在Insert和Find方法中都有一个数据类型ExportData,分别作为输入与输出,而现在有这样的需求:

ExportData需要仅支持整型(long),浮点型(double),字符串(string)以及二进制(void*, size)4种类型的操作(不支持int或float)

ExportData需要考虑结构的尺寸,尽量减少空间冗余

即使对以上4种不同数据类型进行操作,还是希望在从ExportData中Get或Set真实数据时,使用的方法能统一

当调用者尝试使用了以上4种类型以外的数据类型时,能通过返回错误让调用方知道类型不匹配

需求描述完毕,怎么做?如何去定义和实现ExportData?是不是很简单,第一感觉马上就能解决问题,而且有n种方法。

第一种方案,为ExportData定义GetData和SetData方法,并且为4种类型分别重载方法,代码如下:

class ExportData
{
public:
long GetData() {
return m_lData;
}
void SetData(long data) {
m_lData = data;
}
string GetData() {
return m_strData;
}
void SetData(string data) {
m_strData = data;
}
// ...... overload the other two types

private:
long m_lData;
string m_strData;
// ...... overload the other two types
};


马上发现问题了,首先GetData方法只通过返回值无法重载,但马上想到我们可以稍微改动下解决这个问题:

void GetData(long& data) {
data = m_lData;
}
void SetData(long data) {
m_lData = data;
}
void GetData(string& data) {
data = m_strData;
}
void SetData(const string& data) {
m_strData = data;
}


但仔细看下还是有问题,没有满足需求2中的要求,即使用户使用的是整型数据,其他三种数据类型在结构中还是存在,内部数据有冗余。

那使用类模板不就可以解决这个问题吗?代码如下所示:

template<typename T>
class ExportData
{
public:
T GetData() {
return m_lData;
}
void SetData(T data) {
m_data = data;
}
private:
T m_data;
};


如此简单,这样就没有冗余了,但是这样却把所有只要支持赋值操作的类型都支持了,不满足需求1。很多人这时肯定会想到,这时使用下traits不就能解决这个问题了吗?

template<typename T>
class ExportData
{
public:
RESULT GetData(T& data) {
return ERROR;
}
RESULT SetData(const T& data) {
return ERROR;
}
};
template<>
class ExportData<long>
{
public:
RESULT GetData(long& data) {
data = m_data;
return OK;
}
RESULT SetData(const long& data) {
m_data = data;
return OK;
}
private:
long m_data;
};

template<>
class ExportData<double>
...... // just like the implementation of long
template<>
class ExportData<string>
...... // just like the implementation of long
template<>
class ExportData<Binary>
...... // just like the implementation of long


满足需求1仅支持四种类型,满足需求2没有冗余,满足需求3统一的调用形式,但是对于需求4的问题,有点问题,因为当你使用int或者float时仍旧支持,也就是只要数据间可以隐式转换,就不会返回错误提示调用方,那就再改善下吧:

template<typename T>
struct TypeTraits
{
static const DATA_TYPE field_type = TYPE_UNSUPPORTED;
};
template<>
struct TypeTraits<std::string>
{
static const DATA_TYPE field_type = TYPE_UTF8;
};
template<>
struct TypeTraits<long>
{
static const DATA_TYPE field_type = TYPE_INEGER;
};
template<>
struct TypeTraits<double>
{
static const DATA_TYPE field_type = TYPE_REAL;
};
template<>
struct TypeTraits<Binary>
{
static const DATA_TYPE field_type = TYPE_BINARY;
};


以上先通过Traits的方法获得一个可以用来判断是否是我们支持的数据类型的方式,成立则不支持,不成立则支持,判断方式如下:

TypeTraits<long>::field_type == TYPE_UNSUPPORTED


然后ExportData如下实现:

template<typename T>
class ExportData
{
public:
RESULT GetData(T& data) {
return ERROR;
}
RESULT SetData(const T& data) {
return ERROR;
}
};
template<>
class ExportData<long>
{
public:
RESULT GetData(long& data) {
if (TypeTraits<long>::field_type == TYPE_UNSUPPORTED) {
return ERROR;
}
data = m_data;
return OK;
}
RESULT SetData(const long& data) {
m_data = data;
return OK;
}
private:
long m_data;
};


现在只有这四种类型会被支持,其他类型都会返回错误,似乎所有的需求都支持了,那这就是最终的解决方案吗?

不是!

我们忽略了ExportData在哪被使用了,它被用在了接口类的virtual方法中了,而由于C++编译的一些特性,C++语言本身是不支持虚函数本身又是模板函数的,也就是说以下接口类的定义编译绝对通不过:

class IContainer
{
public:
template<typename T>
virtual RESULT Insert(const std::string& key, const ExportData<T>& data) = 0;

virtual RESULT Delete(const std::string& key) = 0;

template<typename T>
virtual RESULT Find(const std::string& key, ExportData<T>& data) = 0;
};


而IContainer本身不是只跟某个类型相关的,也就是不可能把IContainer定义成模板类。怎么办呢?

由于virtual函数不能同时又是模板函数,所以ExportData类不能定义为模板类,那能尝试把ExportData类的Get和Set方法设置为模板方法来解决吗?这样做还是会存在一个问题,由于不能使模板类,那类成员变量的数据怎么去支持4种类型呢?

答案是都处理成类型无关的二进制数据,也就是说保存数据的首地址以及数据大小,在Get和Set时根据当前类型通过memcpy进行拷贝来转换成指定类型。

看下代码吧,TypeTraits的定义跟上面一样,这里就不重复了:

class ExportData
{
public:
ExportData() : _data(NULL), _size(0){}
ExportData(const ExportData& data) {
_data = NULL;
_size = 0;
AssignData(data._data, data._size);
_type = data._type;
}
~ExportData() {
if (_data) {
delete[] _data;
_data = NULL;
}
}
ExportData& operator=(const ExportData& data) {
this->AssignData(data._data, data._size);
this->_type = data._type;
return *this;

template<typename T>
RESULT SetData(const T& data) {
if (TypeTraits<T>::field_type == TYPE_UNSUPPORTED) {
return ERROR;
}
AssignData((const char*)&data, sizeof(T));
_type = TypeTraits<T>::field_type;
return OK;
}
template<>
RESULT SetData<std::string>(const std::string& data) {
AssignData(data.c_str(), data.size());
_type = TYPE_UTF8;
return OK;
}
template<>
RESULT SetData<Binary>(const Binary& data) {
AssignData(data.GetBlobAddr(), data.GetSize());
_type = TYPE_BLOB;
return OK;
}

template<typename T>
RESULT GetData(T& data) const {
if (TypeTraits<T>::field_type == TYPE_UNSUPPORTED ||
_data == NULL ||
TypeTraits<T>::field_type != _type) {
return ERROR;
}
memcpy(&data, _data, _size);
return OK;
}
template<>
RESULT GetData<std::string>(std::string& data) const {
if (TYPE_UTF8 != _type || _data == NULL) {
data = "";
return ERROR;
}
data.assign(_data, _size);
return OK;
}
template<>
RESULT GetData<Binary>(Binary& data) const {
if (TYPE_BLOB != _type || _data == NULL) {
data.SetBlobData(NULL, 0);
return ERROR;
}
data.SetBlobData(_data, _size);
return OK;
}
private:
void AssignData(const char* data, unsigned int size) {
if (_data) {
delete[] _data;
_data = NULL;
}
_size = size;
_data = new char[size];
memcpy(_data, data, _size);
}
char*  _data;
unsigned long _size;
DATA_TYPE _type;
};


这就是当时的解决方案,可以算是满足了前面提出的4点需求,调用的时候代码如下:

ExportData data;
RESULT res = OK;
res = data.SetData<string>("DataTest");
assert(OK == res);

string str;
res = data.GetData<string>(str);
assert(OK == res);

res = data.SetData<long>(111);
long  ldata = 0;
res = data.GetData<long>(ldata);
assert(OK == res);


我想肯定还有更好的解决方法,比如可以尝试在编译时就提示类型不支持而不是在运行时通过返回错误来提示。如果有更好的解决方案,欢迎一起讨论。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: