以下是 John Maddock 和 Steve Cleary 发表在 2000 年 10 月号 Dr Dobb's Journal 杂志上的文章 “C++ Type traits” 的更新版本。
泛型编程(编写可用于满足一组需求的任何数据类型的代码)已成为提供可重用代码的首选方法。然而,在泛型编程中有时“泛型”还不够好 - 有时类型之间的差异太大,无法实现高效的泛型实现。 这时 traits 技术变得重要 - 通过将需要在类型基础上考虑的属性封装在 traits 类中,我们可以最大限度地减少必须因类型而异的代码量,并最大限度地提高泛型代码量。
考虑一个例子:当处理字符串时,一个常见的操作是确定空终止字符串的长度。 显然,可以编写泛型代码来执行此操作,但事实证明,有更有效的方法可用:例如,C 库函数 strlen
和 wcslen
通常用汇编语言编写,并且在适当的硬件支持下,可能比用 C++ 编写的泛型版本快得多。 C++ 标准库的作者意识到了这一点,并将 char
和 wchar_t
的属性抽象到类 char_traits
中。 使用字符串的泛型代码可以简单地使用 char_traits<>::length
来确定空终止字符串的长度,并且确信 char_traits
的特化将使用最合适的方法。
类 char_traits
是将类型特定属性集合包装在一个类中的经典示例 - Nathan Myers 称之为 行李类[1]。 在 Boost type-traits 库中,我们[2] 编写了一组非常具体的 traits 类,每个类都封装了 C++ 类型系统中的单个 trait; 例如,类型是指针类型还是引用类型? 或者类型是否具有平凡构造函数或 const 限定符? type-traits 类共享统一的设计:如果类型具有指定的属性,则每个类都从类型 true_type 继承,否则从 false_type 继承。 正如我们将展示的那样,这些类可以在泛型编程中用于确定给定类型的属性,并引入适用于该情况的优化。
type-traits 库还包含一组对类型执行特定转换的类; 例如,它们可以从类型中删除顶层 const 或 volatile 限定符。 每个执行转换的类都定义一个 typedef 成员 type
,它是转换的结果。 所有 type-traits 类都在命名空间 boost
内定义; 为了简洁起见,在给出的大多数代码示例中省略了命名空间限定。
type-traits 库中包含的类太多,无法在此处给出完整的实现 - 有关完整详细信息,请参见 Boost 库中的源代码 - 但是,大多数实现无论如何都是相当重复的,因此在这里我们只给您展示一些类的实现方式。 从库中可能最简单的类开始,is_void<T>
仅当 T
是 void
时才从 true_type
继承。
template <typename T> struct is_void : public false_type{}; template <> struct is_void<void> : public true_type{};
在这里,我们定义了模板类 is_void
的主要版本,并在 T
是 void
时提供完全特化。 虽然模板类的完全特化是一项重要技术,但有时我们需要一种介于完全通用解决方案和完全特化之间的解决方案。 这正是标准委员会定义部分模板类特化的情况。 例如,考虑类 boost::is_pointer<T>
:这里我们需要一个处理 T 不是指针的所有情况的主要版本,以及一个处理 T 是指针的所有情况的部分特化
template <typename T> struct is_pointer : public false_type{}; template <typename T> struct is_pointer<T*> : public true_type{};
部分特化的语法有些神秘,很容易占据一篇独立的文章; 像完全特化一样,为了为类编写部分特化,您必须首先声明主模板。 部分特化在类名后包含一个额外的 <...>,其中包含部分特化参数; 这些参数定义将绑定到该部分特化而不是默认模板的类型。 关于什么可以出现在部分特化中的规则有些复杂,但作为经验法则,如果您可以合法地编写以下形式的两个函数重载
void foo(T); void foo(U);
那么您也可以编写以下形式的部分特化
template <typename T> class c{ /*details*/ }; template <typename T> class c<U>{ /*details*/ };
这条规则绝不是万无一失的,但它相当容易记住,并且足够接近实际规则,可以在日常使用中发挥作用。
作为部分特化的更复杂示例,请考虑类 remove_extent<T>
。 此类定义一个 typedef 成员 type
,它与 T 的类型相同,但删除了任何顶层数组边界; 这是对类型执行转换的 traits 类的示例
template <typename T> struct remove_extent { typedef T type; }; template <typename T, std::size_t N> struct remove_extent<T[N]> { typedef T type; };
remove_extent
的目的是:想象一个泛型算法,它将数组类型作为模板参数传递,remove_extent
提供了一种确定数组底层类型的方法。 例如 remove_extent<int[4][5]>::type
将评估为类型 int[5]
。 此示例还表明,部分特化中的模板参数数量不必与默认模板中的数量匹配。 但是,出现在类名后的参数数量必须与默认模板中参数的数量和类型匹配。
作为如何使用 type traits 类的示例,请考虑标准库算法 copy
template<typename Iter1, typename Iter2> Iter2 copy(Iter1 first, Iter1 last, Iter2 out);
显然,编写一个适用于所有迭代器类型 Iter1
和 Iter2
的通用 copy 版本没有问题; 但是,在某些情况下,可以通过调用 memcpy
来最好地执行复制操作。 为了使用 memcpy
实现 copy,需要满足以下所有条件
Iter1
和 Iter2
都必须是指针。Iter1
和 Iter2
都必须指向相同的类型 - 排除 const 和 volatile 限定符。Iter1
指向的类型必须具有平凡赋值运算符。通过平凡赋值运算符,我们指的是该类型要么是标量类型[3],要么
如果满足所有这些条件,则可以使用 memcpy
而不是使用编译器生成的赋值运算符来复制类型。 type-traits 库提供了一个类 has_trivial_assign
,使得 has_trivial_assign<T>::value
仅当 T 具有平凡赋值运算符时才为 true。 这个类对于标量类型“可以正常工作”,但必须为也恰好具有平凡赋值运算符的类/结构类型显式特化。 换句话说,如果 has_trivial_assign 给出了错误的答案,它将给出“安全”的错误答案 - 即不允许平凡赋值。
使用 memcpy
的优化版本 copy 的代码在 示例中给出。 该代码首先定义一个模板函数 do_copy
,它执行“慢但安全”的复制。 传递给此函数的最后一个参数可以是 true_type
或 false_type
。 之后,有一个使用 memcpy
的 do_copy 重载:这次迭代器实际上需要是指向相同类型的指针,并且最后一个参数必须是 true_type
。 最后,copy
版本调用 do_copy
,传递 has_trivial_assign<value_type>()
作为最后一个参数:这将根据需要调度到优化版本,否则将调用“慢但安全版本”。
这些专栏中经常重复“过早优化是万恶之源”[4]。 因此,必须提出问题:我们的优化是否过早? 为了说明这一点,表 1 显示了我们的 copy 版本与传统泛型 copy[5] 相比的计时。
显然,在这种情况下,优化有所作为; 但是,公平地说,计时是为了排除缓存未命中效应 - 如果没有这一点,算法之间的准确比较将变得困难。 但是,也许我们可以为过早优化规则添加一些警告
表 1.1. 使用 `copy<const T*, T*>` 复制 1000 个元素所花费的时间(以微秒为单位)
版本 |
T |
时间 |
---|---|---|
“优化”复制 |
char |
0.99 |
传统复制 |
char |
8.07 |
“优化”复制 |
int |
2.52 |
传统复制 |
int |
8.02 |
优化的 copy 示例展示了如何使用 type traits 在编译时执行优化决策。 type traits 的另一个重要用途是允许代码编译,否则除非使用过多的部分特化,否则代码将无法编译。 这可以通过将部分特化委托给 type traits 类来实现。 我们关于这种用法的示例是一个可以保存引用的 pair[6]。
首先,让我们检查 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” 目前无法保存引用,因为构造函数需要获取对引用的引用,这目前是非法的 [7]。 让我们考虑一下构造函数的参数必须是什么,才能允许 “pair” 保存非引用类型、引用和常量引用
稍微熟悉 type traits 类,我们就可以构建一个单一的映射,使我们能够从包含类的类型确定参数的类型。 type traits 类提供了一个转换 add_reference,它向其类型添加引用,除非它已经是引用。
表 1.3. 使用 add_reference 合成正确的构造函数类型
|
|
|
---|---|---|
T |
const T |
const T & |
T & |
T & [8] |
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
!
相同的扩展可以使用 pair
的部分模板特化来完成,但是以这种方式特化 pair
将需要三个部分特化,加上主模板。 Type traits 允许我们定义一个自动调整自身以适应任何这些部分特化的单个主模板,而不是强力部分特化方法。 以这种方式使用 type traits 允许程序员将部分特化委托给 type traits 类,从而使代码更易于维护和理解。
我们希望在本文中,我们能够让您了解 type-traits 的全部内容。 更完整的可用类列表在 boost 文档中,以及更多使用 type traits 的示例。 模板使 C++ 用户能够利用泛型编程带来的代码重用优势; 希望本文已经表明,泛型编程不必降低到最低的共同标准,并且模板可以像泛型一样最佳。
作者要感谢 Beman Dawes 和 Howard Hinnant 在撰写本文时提供的有益评论。