Boost C++ 库

...是世界上最受推崇和设计精良的 C++ 库项目之一。 Herb SutterAndrei AlexandrescuC++ 编码规范

PrevUpHomeNext

背景和教程

以下是对 John Maddock 和 Steve Cleary 在 2000 年 10 月刊的 Dr Dobb's Journal 中发表的文章“C++ 类型特征”的更新版本。

泛型编程(编写适用于满足一组要求的任何数据类型的代码)已成为提供可重用代码的首选方法。然而,在泛型编程中,有时“泛型”并不足以满足需求——有时类型之间的差异太大,无法实现高效的泛型实现。这时,特征技术就变得很重要——通过将需要在类型基础上考虑的那些属性封装在特征类中,我们可以最大程度地减少代码因类型不同而需要变化的部分,并最大程度地增加泛型代码的数量。

考虑一个示例:在处理字符字符串时,一个常见的操作是确定以 null 结尾的字符串的长度。显然,可以编写能够执行此操作的泛型代码,但事实证明,存在更有效的方法:例如,C 库函数 strlenwcslen 通常是用汇编语言编写的,并且在硬件支持的情况下,其速度可能比用 C++ 编写的泛型版本快得多。C++ 标准库的作者意识到了这一点,并将 charwchar_t 的属性抽象到了 char_traits 类中。处理字符字符串的泛型代码只需使用 char_traits<>::length 来确定以 null 结尾的字符串的长度,确保 char_traits 的专门化将使用它们可用的最合适方法。

类型特征

char_traits 类是将一组特定类型属性封装在一个类中的一个经典示例——Nathan Myers 将其称为 行李类[1]。在 Boost 类型特征库中,我们[2] 编写了一组非常具体的特征类,每个类都封装了 C++ 类型系统中的一个特征;例如,类型是指针还是引用类型?或者类型是否具有平凡的构造函数,或者具有 const 限定符?类型特征类共享一个统一的设计:如果类型具有指定的属性,则每个类都继承自类型 true_type,否则继承自 false_type。正如我们将展示的,这些类可以在泛型编程中用来确定给定类型的属性,并引入适合该情况的优化。

类型特征库还包含一组执行特定类型转换的类;例如,它们可以从类型中移除顶层 const 或 volatile 限定符。每个执行转换的类都定义了一个名为 type 的单 typedef 成员,它是转换的结果。所有类型特征类都在命名空间 boost 内定义;为简洁起见,大多数代码示例中都省略了命名空间限定符。

实现

类型特征库包含的单独类太多,无法在此给出完整的实现——有关完整详细信息,请参阅 Boost 库中的源代码——但是,大多数实现相当重复,因此我们只介绍一些类的实现方式。从库中可能最简单的类 is_void<T> 开始,它仅当 Tvoid 时才继承自 true_type

template <typename T>
struct is_void : public false_type{};

template <>
struct is_void<void> : public true_type{};

在这里,我们定义了模板类 is_void 的主要版本,并在 Tvoid 时提供了一个完全专门化。虽然模板类的完全专门化是一项重要的技术,但有时我们需要一个介于完全泛型解决方案和完全专门化之间的解决方案。这正是标准委员会定义部分模板类专门化的场景。例如,考虑类 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*/ };

的partial specialization。这条规则并非万无一失,但它很容易记住,并且与实际规则足够接近,在日常使用中非常有用。

作为部分专门化的更复杂示例,考虑类 remove_extent<T>。此类定义了一个名为 type 的单 typedef 成员,其类型与 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);

显然,编写适用于所有迭代器类型 Iter1Iter2 的泛型版本 copy 没有任何问题;但是,在某些情况下,复制操作可以通过调用 memcpy 最好地完成。为了用 memcpy 实现 copy,所有以下条件都必须满足。

所谓平凡的赋值运算符,是指类型要么是标量类型[3],要么是

如果满足所有这些条件,则可以使用 memcpy 来复制类型,而不是使用编译器生成的赋值运算符。类型特征库提供了一个类 has_trivial_assign,使得 has_trivial_assign<T>::value 仅当 T 具有平凡的赋值运算符时才为真。此类对于标量类型“正常工作”,但必须针对也具有平凡赋值运算符的类/结构类型进行显式专门化。换句话说,如果 has_trivial_assign 给出了错误的答案,它将给出“安全”的错误答案——即不允许平凡赋值。

使用memcpy对复制进行优化的代码版本在示例中给出。该代码首先定义一个模板函数do_copy,它执行“缓慢但安全”的复制。传递给此函数的最后一个参数可以是true_typefalse_type。之后是do_copy的重载,它使用memcpy:这次迭代器需要实际指向同一类型,最后一个参数必须是true_type。最后,copy的版本调用do_copy,并将has_trivial_assign<value_type>()作为最后一个参数传递:这将在适当的情况下分派到优化版本,否则将调用“缓慢但安全版本”。

值得吗?

在这些专栏中,人们经常重复“过早优化是万恶之源” [4]。因此,必须问一个问题:我们的优化是否过早?为了将这一点放在一个角度,我们在表 1 中展示了我们版本的复制与传统泛型复制 [5] 的计时结果。

显然,优化在这种情况下确实有所不同;但,公平地说,计时结果经过加载以排除缓存未命中效应——如果没有这一点,算法之间的准确比较会变得困难。但是,也许我们可以对过早优化规则添加一些注意事项

表 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) { }
};

现在,这个“对”不能保存引用,因为它目前处于这种状态,因为构造函数将需要接受对引用的引用,而这在目前是非法的 [7]。让我们考虑一下构造函数的参数必须是什么,才能允许“对”保存非引用类型、引用和常量引用

表 1.2. 所需的构造函数参数类型

T1 的类型

初始化构造函数的参数类型

T

const T &

T &

T &

const T &

const T &


对类型特征类有所了解后,我们可以构建一个单一映射,它允许我们根据所包含类的类型来确定参数的类型。类型特征类提供了转换 add_reference,它将引用添加到其类型,除非它已经是引用。

表 1.3. 使用 add_reference 合成正确的构造函数类型

T1 的类型

const T1 的类型

add_reference<const T1>::type 的类型

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 在撰写本文时提供的宝贵意见。

参考文献
  1. Nathan C. Myers,C++ Report,1995 年 6 月。
  2. 类型特征库基于 Steve Cleary、Beman Dawes、Howard Hinnant 和 John Maddock 的贡献:它可以在 www.boost.org 上找到。
  3. 标量类型是指算术类型(即内置整数或浮点类型)、枚举类型、指针、指向成员的指针,或其中一种类型的常量或易变限定版本。
  4. 这句引用来自 Donald Knuth,ACM Computing Surveys,1974 年 12 月,第 268 页。
  5. 测试代码作为 boost 实用库的一部分提供(参见 algo_opt_examples.cpp),代码使用 gcc 2.95 编译,所有优化都已开启,测试在运行 Microsoft Windows 98 的 400MHz Pentium II 机器上进行。
  6. John Maddock 和 Howard Hinnant 已向 Boost 提交了“compressed_pair”库,它使用与这里描述的类似技术来保存引用。他们的对也使用类型特征来确定任何类型是否为空,并将派生而不是包含以节省空间——因此名称为“压缩”。
  7. 这实际上是 C++ 核心语言工作组的一个问题(问题 #106),由 Bjarne Stroustrup 提交。暂定的解决方案是允许“对 T 的引用”与“对 T 的引用”具有相同的含义,但仅在模板实例化中,以与多个 cv 限定符类似的方式。
  8. 对于那些想知道为什么这不能是 const 限定的,请记住引用始终是隐式常量(例如,您不能重新分配引用)。还要记住,“const T &” 是完全不同的东西。出于这个原因,在作为引用的模板类型参数上的 cv 限定符会被忽略。

PrevUpHomeNext