以下是 John Maddock 和 Steve Cleary 撰写的文章“C++ 类型特征”的更新版本,该文章发表于 2000 年 10 月的《Dr Dobb's Journal》杂志。
泛型编程(编写适用于满足一组要求的任何数据类型的代码)已成为提供可重用代码的首选方法。但是,在泛型编程中,有时“泛型”并不够好——有时类型之间的差异太大,无法进行有效的泛型实现。这时,特征技术就变得很重要了——通过将需要逐个类型考虑的属性封装在特征类中,我们可以最大限度地减少必须根据不同类型而变化的代码量,并最大限度地增加泛型代码量。
考虑一个例子:在处理字符字符串时,一个常见的操作是确定以 null 结尾的字符串的长度。显然,可以编写可以执行此操作的泛型代码,但事实证明,还有更有效的方法可用:例如,C 库函数strlen
和wcslen
通常是用汇编语言编写的,并且在合适的硬件支持下,其速度可能比用 C++ 编写的泛型版本快得多。C++ 标准库的作者意识到了这一点,并将char
和wchar_t
的属性抽象到char_traits
类中。与字符字符串一起工作的泛型代码可以简单地使用char_traits<>::length
来确定以 null 结尾的字符串的长度,并确信char_traits
的特化将使用最合适的方法。
char_traits
类是将一组类型特定属性包装到单个类中的一个经典示例——Nathan Myers 称之为行李类[1]。在 Boost 类型特征库中,我们[2]编写了一组非常具体的特征类,每个类都封装了 C++ 类型系统中的单个特征;例如,类型是指针类型还是引用类型?或者类型是否具有平凡构造函数或 const 限定符?类型特征类共享统一的设计:如果类型具有指定的属性,则每个类都继承自true_type类型,否则继承自false_type。我们将展示这些类如何在泛型编程中用于确定给定类型的属性并引入适合该情况的优化。
类型特征库还包含一组对类型执行特定转换的类;例如,它们可以从类型中移除顶层 const 或 volatile 限定符。每个执行转换的类都定义一个单一的 typedef 成员type
,它是转换的结果。所有类型特征类都在命名空间boost
内定义;为简洁起见,在给出的大多数代码示例中都省略了命名空间限定。
类型特征库中包含的单独类太多,无法在此处提供完整的实现——有关完整详细信息,请参见 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 的类型相同,但移除了任何顶层数组边界;这是一个执行类型转换的特征类的示例。
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]
。此示例还表明,部分特化中的模板参数数量不必与默认模板中的参数数量匹配。但是,类名后出现的参数数量必须与默认模板中的参数数量和类型匹配。
作为如何使用类型特征类的示例,请考虑标准库算法 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
复制类型,而不是使用编译器生成的赋值运算符。类型特征库提供了一个类has_trivial_assign
,使得只有当 T 具有平凡赋值运算符时,has_trivial_assign<T>::value
才为 true。这个类对于标量类型“可直接使用”,但必须为也恰好具有平凡赋值运算符的类/结构体类型显式特化。换句话说,如果has_trivial_assign给出错误的答案,它将给出“安全”的错误答案——不允许平凡赋值。
在示例中给出了一个使用memcpy
(在适当情况下)的优化版复制代码。代码首先定义了一个模板函数do_copy
,它执行“缓慢但安全”的复制。传递给此函数的最后一个参数可以是true_type
或false_type
。接下来是`do_copy`的重载版本,它使用memcpy
:这次迭代器必须实际是指向相同类型的指针,并且最后一个参数必须是true_type
。最后,copy
版本调用do_copy
,并将has_trivial_assign<value_type>()
作为最后一个参数传递:这将在适当的情况下调度到优化版本,否则将调用“缓慢但安全”版本。
在这些专栏中经常重复提到“过早优化是万恶之源”[4]。因此,必须提出一个问题:我们的优化是否过早?为了说明这一点,我们将我们版本的复制与传统的泛型复制[5]的计时结果进行了比较,如表1所示。
显然,在这种情况下,优化确实有所不同;但是,公平地说,计时结果排除了缓存未命中效应——没有这一点,算法之间的准确比较就变得困难。但是,也许我们可以对过早优化规则添加一些注意事项。
表1.1 使用`copy<const T*, T*>`复制1000个元素所需的时间(单位:微秒)
版本 |
T |
时间 |
---|---|---|
“优化”后的复制 |
char |
0.99 |
传统复制 |
char |
8.07 |
“优化”后的复制 |
int |
2.52 |
传统复制 |
int |
8.02 |
优化的复制示例展示了如何使用类型特性在编译时执行优化决策。类型特性的另一个重要用途是允许编译否则无法编译的代码,除非使用了过多的局部特化。这可以通过将局部特化委托给类型特性类来实现。我们对此类用法的示例是一个可以保存引用的对[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”保存非引用类型、引用和常量引用。
对类型特性类稍加了解,我们就可以构建一个单一的映射,允许我们根据包含类的类型确定参数的类型。类型特性类提供了一个转换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
,需要三个局部特化,加上主模板。类型特性允许我们定义一个单一的主模板,它可以自动调整自身以适应任何这些局部特化,而不是采用蛮力的局部特化方法。以这种方式使用类型特性允许程序员将局部特化委托给类型特性类,从而使代码更易于维护和理解。
我们希望本文能够让你了解类型特性的相关内容。Boost文档中提供了更完整的可用类列表,以及使用类型特性的更多示例。模板使C++用户能够利用泛型编程带来的代码重用优势;希望本文能够说明泛型编程不必降低到最低公分母,并且模板可以同时是最佳的和通用的。
作者要感谢Beman Dawes和Howard Hinnant在准备本文时提供的宝贵意见。