C++类型萃取
泛型编程(编写能够适用于任意类型以满足一系列需求的代码)已经成为提供可重用代码的流行方法。尽管如此,很多时候在泛型编程中“泛”却不是很好-有时候类型之间的差异太大以至于没法编写出适用于所有类型的泛型实现。这就是为何萃取技术变得重要-将那些需要在不同的类型之间考虑的特性封装到萃取类中,就能使得为类型差异而编写的代码最少化,而最大化泛型代码。
考虑这个例子:处理字符串的时候,一个通用操作就是获取以null结尾的字符串的长度。我们当然能够编写泛型代码实现它,但现实情况是有太多高效的方法了:例如,C的库函数strlen和wcslen就常常以汇编的形式实现,加上一些合适的硬件支持它们会比C++编写的泛型算法快很多。C++标准库的作者也意识到这一点,所以char和wchar_t被抽象到萃取类char_traits中。这样处理字符串的泛型代码可以很简单的使用char_traits<>::length来获取以null结尾字符串的长度,因为我们可以放心的知道char_traits的特化会选择最合适的方法去实现它。
类型萃取
char_traits类是一个将类型专属特性包装到一个独立的类的典型泛例-Nathan Myers将这种类命名为行李类(baggage class)。在Boost的type-traits库中, 大牛们设计了一系列非常详细的萃取类,每一种都封装了C++类型系统中的单独萃取;例如,一个类型是指针还是引用?一个类型是否具有缺省构造函数?是否有const限定符?所有的type-traits都有一个统一的设计:每一个类有且只有一个成员value,这是一个编译期的常量,如果被萃取的类型具有指定的属性,那么它的值是true,反之是false。在接下来的例子中,这些类将被使用在泛型程序中以判定给定类型的特性和随之而来的优化。
type-traits库中还有一系列类用来实现特定的类型转换;例如,它们可以将最外层的const或者volatile限定符移除。每一个实现类型转换的类都定义了唯一一个typedef的成员type, 用来保存类型转换的结果。所有的类型萃取类都定义在boost这个名字空间里;简单起见,namespace关键字在大部分实例中会被省略。
实现
type-traits库中包含的类非常多以至于本文无法一一列举,感兴趣的朋友可以在Boost库中找到所有源码。尽管如此,大部分实现都非常类似,所以这里我们将列举一些类实现的概要。我们将从最简单的类开始-is_void<T>, 它有一个成员value只有在T是void类型的时候才是true。
template<typename T>
structis_void
{static const bool value = false; };
template<>
structis_void<void>
{static const bool value = true; };
偏特化的语法有点晦涩并且很容易单独展开为一篇文章;就像特化一样,为一个类编写偏特化,必须首先声明参数模板。但是偏特化有一个额外的<…>紧接着类名之后用来指定偏特化参数;这些参数定义了这个偏特化绑定的类型。偏特化的规则有些令人费解,但是它与函数重载的形式有相似之处:
void foo(T);
void foo(U);
我们也能写出相似的偏特化:
template <typename T>
class c{ /*details*/ };
template <typename T>
class c<U>{ /*details*/ };
这些规则当然不是完全可靠的,但也足够应付日常使用了。
我们来看偏特化的一个复杂些的例子-remove_bounds<T>。这个类定义了唯一一个typedef成员type,type的类型与T的一样,但是会移除任意最外层的数组边界;下面是一个实现类型转换萃取类的例子:
template <typename T>
struct remove_bounds
{ typedef T type; };
template <typename T, std::size_t N>
struct remove_bounds<T[N]>
{ typedef T type; };
remove_bound的目的:试想一个泛型算法接受一个数组类型做为模板参数,remove_bounds提供对该数组元素类型的解析。例如remove_bounds<int[4][5]>::type将会推断出类型int[5]。这个例子也显示了偏特化中模板参数的个数不一定与默认模板中的个数一致。尽管如此,类名后面的参数个数必须要和默认模板中的个数一致。
优化的copy算法
我们以标准模板库中的算法copy为示例来看看怎样使用萃取:
template<typename Iter1, typename Iter2>
Iter2 copy(Iter1 first, Iter1 last, Iter2 out);
显然,编写一个适用于任何迭代器类型Iter1和Iter2的拷贝算法不会有任何问题;但是很多情况下使用memcpy来实现copy是更为高效的方法。要使用memcpy必须满足以下条件:
- 迭代器Iter1和Iter2都必须是指针类型
- Iter1与Iter2都必须指向同一类型-并且不能有const和volatile限定符
- Iter1指向的类型必须有缺省赋值操作符
缺省赋值操作符意味着这个类型要么是一个标量类型,要么具有以下条件:
- 这个类型没有自定义的赋值操作符
- 这个类型没有引用成员变量
- 这个类型的所有基类,所有成员变量都必须具有缺省赋值操作符
如果所有条件都被满足,那么这个类型就可以使用memcpy来拷贝,而不是编译器产生的缺省赋值操作符。type-traits库提供了has_traivial_assign这个类,使用has_trivial_assign<T>::value可以知道T是否具有缺省赋值操作符。这个类只能“作用”于标量类型, 但是需要为那些碰巧也有缺省赋值操作符的类型做显式特化。换句话说,如果has_trivial_assign给出错误的答案,那也将是个“安全”的错误答案-那个萃取操作不被允许。
使用memcpy优化copy函数的代码在清单1。代码首先定义了一个模板类copier,它接受一个Boolean作为模板参数,成员函数do_copy实现了通用版本的copy(低效但是安全的版本)。接下来我们有一个偏特化版本copier<true>:它也定义了一个静态模板成员函数do_copy,但是这个版本使用的是memcpy来实现优化的copy。
为了完成实现,我们需要知道什么时候用coper<true>::do_copy,何时使用通用版本。源码展示了如何实现这一点。理解代码首先要看copy的源码,先看看两个typedefs v1_t和v2_t。它们使用了std::iterator_traits<Iter1>::value_type来判定迭代器指向的对象类型,得到的结果会传入另一个萃取类remove_cv,从而移除最外层的const或volatile限定符:这样copy就能对这两个类型进行比较,而不用考虑限定符了。接下来,copy声明了一个枚举值can_opt,它将会是coper的模板参数-在这里声明成一个常量只是为了方便-这样这个值就能直接传给copier了。只有当一下所有条件满足can_opt的值才为true:
- 首先两个迭代器要指向同一个类型-使用萃取类is_same可以判断
- 其次两个迭代器都要是真实的指针-使用is_pointer类来获取
- 最后被指向的类型需要有缺省赋值操作符-使用has_trivial_assign
最后我们就能使用can_opt的值作为模板参数传给copier了-这个版本的copy将自动适配任意类型的参数,它会尽可能使用memcpy来执行copy。
这么做值得吗?
许多专栏反复告诫我们“不成熟的优化是万恶之源”。所以我们要问:我们的优化是不成熟的吗?为了有更清晰的认识,我们把优化版本与泛型版本的copy执行时间对比展示在表单1中。
显然在这个例子中优化带来了很大的不同。公平起见,我们在计时中排除了缓存未命中的影响-没有这个因素算法之间的精确比较会变得困难。尽管如此,或许我们可以给出对不成熟优化的一些警示:
- 如果你一开始就使用了正确的算法工作那么不需要优化;某些情况下memcpy就是正确的算法。
- 如果一个组件会被许多不同的地方和不同的人使用那么优化很可能是有价值的-换言之,需要优化的可能性将大大提高。
表单1: 使用copy<const T*, T*>拷贝1000个元素所用的时间 (单位是微秒)
Version | T | Time |
"Optimised" copy | char | 0.99 |
Conventional copy | char | 8.07 |
"Optimised" copy | int | 2.52 |
Conventional copy | int | 8.02 |
引用的“pair”
优化的copy展示了类型萃取如何在编译器实施优化的决策。萃取的另一个重要的使用场景就是只允许有偏特化代码编译通过。实现的途径是把偏特化代理给萃取类。我们的例子就是能持有引用类型的pair。
首先我们测试一下“std::pair”的定义,简单起见,请忽略比较操作符,默认构造函数与模板拷贝构造函数:
template <typename T1, typename T2> struct pair { typedef T1 first_type; typedef T2 second_type; T1 first; T2 second; pair(const T1 & nfirst, const T2 & nsecond) :first(nfirst), second(nsecond) { } };现在,如你所见这个pair类型不能持有引用类型,因为如果模板参数类型是引用的话,拷贝构造函数接受的参数就是引用的引用,这是非法的。我们考虑一下如果pair接受非引用,引用和常量引用类型作为模板参数,拷贝构造函数的参数应该是什么类型:
Type of "T1" | Type of parameter to initializing constructor |
T | const T & |
T & | T & |
const T & | const T & |
只要熟悉类型萃取我们就能构造出从所包含类型推断到参数类型的一一对应关系。萃取类库提供了一个转换“add_reference”,可以将一个类转换为引用类型,除非它本来就是引用。
Type of "T1" | Type of "const T1" | Type of "add_reference<const T1>::type" |
T | const T | const T & |
T & | T & | T & |
const T & | const T & | const T & |
这样我们就能构造出能适配非引用类型,引用类型和常量引用类型的pair了:
template <typename T1, typename T2> struct pair { typedef T1 first_type; typedef T2 second_type; T1 first; T2 second; pair(boost::add_reference<const T1>::type nfirst, boost::add_reference<const T2>::type nsecond) :first(nfirst), second(nsecond) { } };对比较操作符,默认构造函数与模板拷贝构造函数做同样的操作,我们就得到了能持有引用类型的std::pair了!
我们可以用偏特化的方法达到同样的目的,但是那样我们需要三个版本的偏特化。萃取允许我们只需定义一个模板就能自动地适配任意偏特化版本,省去了野蛮的偏特化。这样使用类型萃取可以使得程序员把变特化的工作代理给萃取类,从而写出维护性和易读性更高的代码。
结语
我们希望这边文章可以帮助你们了解关于类型萃取的方方面面。在boost的文档中可以得到萃取类的更详细的代码清单与相关的例子。模板使得C++更好的运用泛型编程带来的代码重用;希望这篇文章展示了泛型编程不需要只为所有类的共同特性而存在,模板与通用类型一样可以被优化。
清单1
namespace detail{
template <bool b>
struct copier
{
template<typename I1, typename I2>
static I2 do_copy(I1 first,
I1 last, I2 out);
};
template <bool b>
template<typename I1, typename I2>
I2 copier<b>::do_copy(I1 first,
I1 last,
I2 out)
{
while(first != last)
{
*out = *first;
++out;
++first;
}
return out;
}
template <>
struct copier<true>
{
template<typename I1, typename I2>
static I2* do_copy(I1* first, I1* last, I2* out)
{
memcpy(out, first, (last-first)*sizeof(I2));
return out+(last-first);
}
};
}
template<typename I1, typename I2>
inline I2 copy(I1 first, I1 last, I2 out)
{
typedef typename
boost::remove_cv<
typename std::iterator_traits<I1>
::value_type>::type v1_t;
typedef typename
boost::remove_cv<
typename std::iterator_traits<I2>
::value_type>::type v2_t;
enum{ can_opt =
boost::is_same<v1_t, v2_t>::value
&& boost::is_pointer<I1>::value
&& boost::is_pointer<I2>::value
&& boost::
has_trivial_assign<v1_t>::value
};
return detail::copier<can_opt>::
do_copy(first, last, out);
}