细说 C++ Traits Classes

最近在看侯捷的《STL源码剖析》,看到第三章有关 traits 的介绍,被搞得一头雾水,看了半天不知所云。为了彻底了解这个技法的原理,硬着头皮上网查了资料,并结合 Scott Meyers 的 《Effective C++》,总算是把 traits 的原理搞明白了:)

什么是 traits?我们先来看下 C++ 之父的回答:

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”. - Bjarne Stroustrup

在 C++ 中,traits 习惯上总是被实现为 struct ,但它们往往被称为 traits classes。Traits classes 的作用主要是用来为使用者提供类型信息。

模板特化(Template Specialization)

为了清晰理解 traits 的原理,我们先来看 traits 使用的关键技术 —— 模板的特化与偏特化。

我们先来看下一函数模板的通用定义:

1
2
3
4
template<typename T>
struct my_is_void {
static const bool value = false;
};

然后,针对 void 类型,我们有以以下的特化版本:

1
2
3
4
template<>
struct my_is_void<void> {
static const bool value = true;
};

测试代码如下:

1
2
3
4
my_is_void<bool> t1;
cout << t1.value << endl; // 输出0
my_is_void<void> t2;
cout << t2.value << endl; // 输出1

当声明 my_is_void<void> t2; 时,使用的是特化版本,故其 value 值为 1。

偏特化(Patial Spcialization)

模板特化时,可以只指定一部分而非所有模板参数,或者是参数的一部分而非全部特性,这叫做模板的偏特化。一个类模板的偏特化本身是一个模板,使用它时用户还必须为那些在特例化版本中未指定的模板参数提供实参。

我们以另一个例子来说明模板的偏特化。先来看通用的原始模板。

1
2
3
4
template<typename T>
struct my_is_pointer {
static const bool value = false;
};

我们对模板参数T进行限制,要求其为一个指针的类型:

1
2
3
4
template<typename T>
struct my_is_pointer<T*> {
static const bool value = true;
};

测试:

1
2
3
4
my_is_pointer<int> p1;
cout << p1.value << endl; // 输出 0,使用原始模板
my_is_pointer<int*> p2;
cout << p2.value << endl; // 输出 1,使偏特化模板,因为指定了 int * 类型的参数

typename 关键字

提问一个问题,以下模板的声明中, classtypename 有什么不同?

1
2
template<class T> class Test;
template<typename T> class Test;

答案:没有不同。然而,C++ 并不总是把 class 和 typename 视为等价。有时候我们一定得使用 typename。
默认情况下,C++ 语言假定通过作用域运算符访问的名字不是类型。因此,如果我们希望使用一个模板类型参数的类型成员,就必须显式告诉编译器该名字是一个类型。我们通过使用关键字 typename 来实现这一点:

1
2
3
4
5
6
7
8
template<typename T>
typename T::value_type top(const T &c)
{
if (!c.empty())
return c.back();
else
return typename T::value_type();
}

top 函数期待一个容器类型的实参,它使用 typename 指明其返回类型,并在 c 中没有元素时生成一个初始值的元素,并返回给调用者。

1
2
3
4
5
vector<int> vec;
vec.push_back(1);
vec.push_back(2);
vec.push_back(3);
cout << top<vector<int> >(vec) << endl;

在这里我们只需要记住一点,当我们希望通知编译器一个名字表示类型时,必须使用关键字 typename,而不能使用 class。

实现 Traits Classes

说完了背景知识,我们正式进入 traits 的关键地带。

我们知道,在 STL 中,容器与算法是分开的,彼此独立设计,容器与算法之间通过迭代器联系在一起。那么,算法是如何从迭代器类中萃取出容器元素的类型的?没错,这正是我们要说的 traits classes 的功能。
迭代器所指对象的类型,称为该迭代器的 value_type。我们来简单模拟一个迭代器 traits classes 的实现。

1
2
3
4
template<class IterT>
struct my_iterator_traits {
typedef typename IterT::value_type value_type;
};

my_iterator_traits 其实就是个类模板,其中包含一个类型的声明。有上面 typename 的基础,相信大家不难理解 typedef typename IterT::value_type value_type; 的含义:将迭代器的value_type 通过 typedefvalue_type

对于my_iterator_traits,我们再声明一个偏特化版本。

1
2
3
4
template<class IterT>
struct my_iterator_traits<IterT*> {
typedef IterT value_type;
};

即如果 my_iterator_traits 的实参为指针类型时,直接使用指针所指元素类型作为 value_type

为了测试 my_iterator_traits 能否正确萃取迭代器元素的类型,我们先编写以下的测试函数。

1
2
3
4
5
6
7
8
9
10
11
void fun(int a) {
cout << "fun(int) is called" << endl;
}

void fun(double a) {
cout << "fun(double) is called" << endl;
}

void fun(char a) {
cout << "fun(char) is called" << endl;
}

我们通过函数重载的方式,来测试元素的类型。

测试代码如下:

1
2
3
4
5
6
my_iterator_traits<vector<int>::iterator>::value_type a;
fun(a); // 输出 fun(int) is called
my_iterator_traits<vector<double>::iterator>::value_type b;
fun(b); // 输出 fun(double) is called
my_iterator_traits<char*>::value_type c;
fun(c); // 输出 fun(char) is called

为了便于理解,我们这里贴出 vector 迭代器声明代码的简化版本:

1
2
3
4
5
6
7
8
9
10
template <class T, ...>
class vector {
public:
class iterator {
public:
typedef T value_type;
...
};
...
};

我们来解释 my_iterator_traits<vector<int>::iterator>::value_type a; 语句的含义。
vector<int>::iteratorvector<int> 的迭代器,该迭代器包含了 value_type 的声明,由 vector 的代码可以知道该迭代器的value_type 即为 int 类型。
接着,my_iterator_traits<vector<int>::iterator> 会采用 my_iterator_traits 的通用版本,即 my_iterator_traits<vector<int>::iterator>::value_type 使用 typename IterT::value_type 这一类型声明,这里 IterTvector<int>::iterator,故整个语句萃取出来的类型为 int 类型。

对 double 类型的 vector 迭代器的萃取也是类似的过程。

my_iterator_traits<char*>::value_type 则使用 my_iterator_traits 的偏特化版本,直接返回 char 类型。

由此看来,通过 my_iterator_traits ,我们正确萃取出了迭代器所指元素的类型。

总结一下我们设计并实现一个 traits class 的过程:
1)确认若干我们希望将来可取得的类型相关信息,例如,对于上面的迭代器,我们希望取得迭代器所指元素的类型;
2)为该信息选择一个名称,例如,上面我们起名为 value_type;
3)提供一个 template 和一组特化版本(例如,我们上面的 my_iterator_traits),内容包含我们希望支持的类型相关信息。

参考资料

  1. https://accu.org/index.php/journals/442
  2. Effective C++,第三版,Scott Meyers 著,侯捷译
  3. STL 源码剖析,侯捷著
  4. http://www.bogotobogo.com/cplusplus/template_specialization_traits.php
  5. http://www.bogotobogo.com/cplusplus/template_specialization_function_class.php