如果你能看完这篇博客,并且能初步了解C++中Trait编程技巧的用法,那么恭喜你!你对于C++的理解已经比别人更深一层次了~O(∩_∩)O。不过Traits技巧我也只是略懂皮毛,这篇博客主要也是说说我的浅显理解,如有错误,敬请谅解 ~

Traits简介

  初次接触到 _Traits_ 是在学习OpenCV的过程中,OpenCV中有一个DataType类,当时很不理解这个类到底是做什么用的,下面是关于它的描述:

When OpenCV library functions need to communicate the concept of a particular data type, they do so by creating an object of type cv::DataType<>. cv::DataType<> itself is a template, and so the actual objects passed around are specializations of this template. This is an example of what in C++ are generally called traits.This allows the cv::DataType<> object to contain both runtime information about the type, as well as typedef statements in its own definition that allow it to refer to the same type at compile time.

  我对这段话的理解是,cv::DataType<>是一个模板类,当OpenCV中库函数需要传递特定数据类型的某些概念信息时,那么就可以通过创建cv::DataType<> 类型的对象来实现。我们使用的不是它本身,而是它特定的实例化对象。C++中这种用法叫做Traits

  说实话,第一次看这一篇章,我也没有看懂cv::DataType<>到底是做什么用的,不过下面讲解一下Traits后,就能明白上面说意思了。

  简单来说,如果我们封装了一个算法,这个算法可能会由于输入数据类型的不同导致算法内部处理逻辑的不同(比如说传入的是int类型我们做一种操作,而传入的是double类型我们将进行另外一种操作),而我们并不想由于这种原因修改算法的封装时,Traits就派上用场了,它可以帮我们很方便的实现功能,而又不破坏函数的封装。
  Traits在开发者中运用特别多,主要也就是为了解决用户的负担,让一些复杂逻辑处理留给开发者来做,用户只需要根据要求调用API函数即可。


一个简单的例子

  比如我们编写了一个模板类,其中有一个sort函数,我们有以下需求:

输入类型参数 操作
int类型 将所有的数加5后排序
double类型 将所有的数减去2.0后排序
1
2
3
4
5
6
7
8
9
template<typename _Tp> class algorithm
{
public:

_Tp* sort(_Tp *array)
{
//do something ......
}
};

  程序在编译时,并不知道模板T是什么类型的参数,这个参数需要在程序运行实例化创建一个对象时,才具体知道T是什么。所以我们不能直接通过if T is xxx,这种方式来处理我们的逻辑。

  一种naive的处理方式为再为类模板添加一个参数,指明传入的参数是什么类型。

1
2
3
4
5
6
7
8
9
10
#define TYPE_INT 1
#define TYPE_DOUBLE 2
#define ..........

template<typename _Tp,int type> class algorithm
{
public:
//do something ......
_Tp* sort(_Tp *array)
};

  我们实例化时方式如下algorithm<int,1>,这样我们的确能实现功能,不过这样就变成了,由用户来指明输入的参数类型。当我们可能有许多的类型需要判断时,我们的输入模板可能会变成这样template<typename _Tp,int type,bool isxxx,int size............>,这样严重的破坏了函数的封装性,用户也并不关心sort()函数内部实现的逻辑,这种判断工作应该交给开发者完成。

  这时候就轮到我们的Traits技巧登场啦,看如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#define TYPE_DEFAULT 0
#define TYPE_INT 1
#define TYPE_DOUBLE 2
#define ..........


template<typename _Tp> class traits
{
public:

static int data_type = TYPE_DEFAULT;
};

template<> class traits<int>
{
public:

static int data_type = TYPE_INT;
};

template<> class traits<double>
{
public:

static int data_type = TYPE_DOUBLE;
};

  上面我们定义的模板类traits都是同一个类,其中第一个没有给定类型参数的为默认的实例化模板,但是后面两个给定了参数(int、double)的为显示具体化,显示具体化优先于常规模板,简单来说也就是如果给定了一个显示具体化的模板,当输入类型为该类型时,它会执行显示具体化模板中的代码,上面traits<int>::data_type = TPYE_INTtraits<double>::data_type = TYPE_DOUBLE。有了traits我们上面定义的函数中可以这样写,而不用修改函数的任何封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#define TYPE_INT 1
#define TYPE_DOUBLE 2
#define ..........

template<typename _Tp,int type> class algorithm
{
public:
//do something ......
_Tp* sort(_Tp *array)
{
switch(traits<_Tp>::data_type)
{
case TYPE_INT:
//do something
case TYPE_DOUBLE:
//do something
........
}
}
};

  通过上面这种技术,我们将程序在运行时候的信息程序编译时候的信息绑定在了一起,这也就是OpenCV中的描述。我们可以很巧妙的判断,一个模板类输入参数类型到底是什么。C++ STL库中的泛型算法就广泛用到了这种编程技巧。

This allows the cv::DataType<> object to contain both runtime information about the type, as well as typedef statements in its own definition that allow it to refer to the same type at compile time.


OpenCV中的DataType类

  Datatype类正是实现了Traits的功能,我们知道OpenCV中的库函数都支持我们输入各种类型的数据,即使数据类型与不是OpenCV的原生数据类型,比如说下面代码,我们通过STL中的复数类型来初始化我们的矩阵,而其并不是OpenCV原生数据类型,这是由于OpenCV定义了DataType<std::complex<_Tp> >这样的一个显示具体化模板。

1
Mat B = Mat_<std::complex<double> >(3, 3);

  OpenCV中DataType类的定义如下,我们知道数据类型不同时枚举体中的各个属性值都不一样,比如说uchar类型的数据channels=1,而Point类型的channels=2。
  我们在调用OpenCV库函数时,不同数据类型的处理过程可能会不一样,所以OpenCV为每一个数据类型都定义其显示具体化模板,通过typedef语句,我们可以很方便的通过调用DataType<_Tp>::value_type来获取当前实例化模板的数据类型。
  OpenCV中的库函数也是通过这样对不同的数据类型进行不同的处理。而我们用户调用时,只需要给定输入类型就OK了,并不需要我们输入一些额外的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template<typename _Tp> class DataType
{
public:
typedef _Tp value_type;
typedef value_type work_type;
typedef value_type channel_type;
typedef value_type vec_type;
enum { generic_type = 1,
depth = -1,
channels = 1,
fmt = 0,
type = CV_MAKETYPE(depth, channels)
};
};

  OpenCV中一些显示具体化的类模板如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

template<> class DataType<bool>
{
public:
typedef bool value_type;
typedef int work_type;
typedef value_type channel_type;
typedef value_type vec_type;
enum { generic_type = 0,
depth = CV_8U,
channels = 1,
fmt = (int)'u',
type = CV_MAKETYPE(depth, channels)
};
};


template<typename _Tp> class DataType< Complex<_Tp> >
{
public:
typedef Complex<_Tp> value_type;
typedef value_type work_type;
typedef _Tp channel_type;

enum { generic_type = 0,
depth = DataType<channel_type>::depth,
channels = 2,
fmt = DataType<channel_type>::fmt + ((channels - 1) << 8),
type = CV_MAKETYPE(depth, channels) };

typedef Vec<channel_type, channels> vec_type;
};



template<> class DataType<Range>
{
public:
typedef Range value_type;
typedef value_type work_type;
typedef int channel_type;

enum { generic_type = 0,
depth = DataType<channel_type>::depth,
channels = 2,
fmt = DataType<channel_type>::fmt + ((channels - 1) << 8),
type = CV_MAKETYPE(depth, channels)
};

typedef Vec<channel_type, channels> vec_type;
};

  最后我们回头看一下最先说的OpenCV中对于DataType数据结构的描述,我们的确是使用了其各种类型实例化对象,也正如OpenCV中描述的那样,我们将编译时的类型信息转换为了与OpenCV兼容的数据类型标识符。

1
2
3
4
5
6
// allocates a 30x40 floating-point matrix
Mat A(30, 40, DataType<float>::type);

Mat B = Mat_<std::complex<double> >(3, 3);
// the statement below will print 6, 2 , that is depth == CV_64F, channels == 2
cout << B.depth() << ", " << B.channels() << endl;

总结

  C++中的模板用法真是博大精深,正是有了Traits技巧,STL中的算法才能够支持各种数据类型,那么如果你想为你的程序写一个通用的算法,而并不想因为要处理不同的输入数据类型而破坏函数的封装时,不妨试试Traits吧!

==参考资料==

【C++模版之旅】神奇的Traits
我对C++ Traits编程技法的一点点理解