C++模板与泛型编程:模板特例化

文章目录

        • 模板特例化
          • 定义函数模板特例化
          • 函数重载与模板特例化
          • 类模板特例化
          • 类模板部分特例化
          • 特例化成员而不是类

模板特例化

​ 编写单一模板,使之对任何可能的模板实参都是最合适的,都能实例化,这并不总是能办到。在某些情况下,通用模板的定义对特定类型是不适合的:通用定义可能编译失败或做得不正确。其他时候,我们也可以利用某些特定知识来编写更高效的代码,而不是从通用模板实例化。当我们不能 (或不希望) 使用模板版本时,可以定义类或函数模板的一个特例化版本。

​ 有以下 compare 函数:

template <typename T> int compare(const T&t1, const T &t2) {
    if(t1 < t2) return -1;
    if(t2 < t1) return 1;
    return 0;
}

很显然,此函数不能处理字符串字面常量,所以我们还需要重载此 compare 来满足这个条件:

template <size_t N, size_t M> int compare(char (&s1)[N], char (&s2)[M]) {
    return strcmp(s1, s2);
}

当我们传递给 compare 一个字符串字面常量或一个数组时,编译器才会调用第二个版本的 compare。如果我们传递给它字符指针,就会调用第一个版本:

const char *p1 = "hi", *p2 = "hhh";
compare(p1, p2);				// 调用第一个版本
compare("hi", "hhh");		// 调用第二个版本

我们无法将一个指针转换为一个数组的引用,因此参数是 p1 和 p2 时,会调用第一个版本的 compare。

​ 为了处理字符指针 (而不是数组),可以为第一个版本的 compare 定义一个模板特例化版本。一个特例化版本就是模板的一个独立定义,在其中一个或多个被指定为特定的类型:

定义函数模板特例化

当我们特例化一个函数模板时,必须为原模板中的每个模板参数都提供实参。为了指出我们正在实例化一个模板,应使用关键字 template 后跟一个空尖括号对(<>)。空尖括号指出我们将为原模板的所有模板参数提供实参:

// compare 的特殊版本,处理字符指针
template <> int compare (const char* const &p1, const char* const &p2) {
    return strcmp(p1, p2);
}

需要注意的是:当我们定义一个特例化版本时,函数参数类型必须与一个先前声明的模板中对于的类型匹配。本例中我们特例化:

template <typename T> int compare(const T&, const T&);

所以,T 为 const char*,我们的函数要求一个指向此类型 const 版本的引用。一个指针类型的 const 版本是一个常量指针而不是指向 const 类型的指针,即 const char* const,而模板实参的引用,所以在特例化版本中,我们的类型为 const char* const &。

函数重载与模板特例化

​ 我们需要清楚一点,一个特例化版本本质上是一个实例,而非函数名的一个重载版本。所以,特例化不影响函数匹配。

建议:模板及其特例化版本应该声明在同一个头文件中。所有同名模板的声明应该放在前面,然后是这些模板的特例化版本。

类模板特例化

​ 类模板也是可以特例化的。作为一个例子,我们将为标准库 hash 模板定义一个特例化版本,可以用它将 Sales_data (三个成员:string bookNo; unsigned units_sold; double revenue; )对象保存在无序容器中。默认情况下,无序容器使用 hash 来组织其元素。为了让我们自己的数据类型也能使用这种默认的组织方式,必须定义 hash 模板的一个特例化版本。一个特例化 hash 类必须定义:

  • 一个重载的调用运算符,它接受一个容器关键字类型的对象,返回一个 size_t
  • 两个类型成员,result_type 和 argument_type,分别是调用运算符的返回类型和参数类型
  • 默认构造函数和拷贝赋值运算符

在定义此特例化版本的 hash 时,唯一复杂的地方是:必须在原模板定义所在的命名空间中特例化它。我们可以向命名空间添加成员,首先打开命名空间:

namespace std {
    
}			// 关闭 std 命名空间,注意没有分号

花括号对之间的任何定义都将成为命名空间 std 的一部分。

​ 然后定义一个能处理 Sales_data 的特例化 hash:

namespace std {
    template <>		// 指明我们正在特例化一个模板,参数类型是 Sales_data
    struct hash <Sales_data> {
        typedef size_t result_type;
        typedef Sales_data argument_type;		// 默认情况下,此类型需要 operator==
        size_t operator()(const Sales_data &s) const;
        // 这里使用合成的拷贝控制成员和默认构造函数
    };
    size_t hash<Sales_data>::operator()(const Sales_data &s) const {
        return hash<string>()(s.bookNo) ^
            hash<unsigned>()(s.units_sold) ^
            hash<double>()(s.revenue);
    }
}		// 关闭 std 命名空间,注意没有分号

默认情况下,为了处理特定关键字类型,无序容器会使用 key_type 对于的特例化 hash 版本和 key_type 上的 operator== 运算符

​ 由于 hash 会使用 Sales_data 的私有成员,所以需要在 Sales_data 中将其声明为友元:

template <class T> class std::hash;		// 友元声明所需要的
class Sales_data {
friend class std::hash<Sales_data>;
    // 其他成员
};
类模板部分特例化

​ 与函数模板不同,类模板的特例化不必为所有模板参数都提供实参。我们可以只指定一部分而非所有模板参数,或是参数的一部分而非全部特性。一个类模板的部分实例化本身是一个模板。使用它时用户必须为那些在特例化版本中未指定的模板参数提供实参。

​ 标准库 remove_reference 就是通过一系列的特例化版本来完成其功能的:

// 原始的、最通用的版本
template <class T> struct remove_reference {
    typedef T type;
}
// 部分特例化版本,将用于左值引用和右值引用
template <class T> struct remove_reference<T&>
	{ typedef T type; }
template <class T> struct remove_reference<T&&>
	{ typedef T type; }

​ 由于一个部分特例化版本本质是一个模板,所以我们首先定义模板参数。同样,部分特例化版本的名字与原模板的名字相同。对每个未完成确定类型的模板实参,在特例化版本的模板参数列表中都有一项与之对应。在类名后,我们要为特例化的模板参数指定实参,这些实参列于模板名之后的尖括号中。这些实参与原始模板中的参数按位置对应。

​ 即部分特例化还指将模板参数中的某个指定为具体的类型

 //general template
template<class T1, class T2> 
class Pair { ... }; 

 //specialization with T2 set to int
template<class T1>
class Pair<T1, int>  { ... };
特例化成员而不是类

​ 我们可以只特例化特定成员函数而不是特例化整个模板。例如,如果 Foo 是一个模板类,包含一个成员 Bar,我们可以只特例化该成员:

template <typename T> struct Foo {
    Foo(const T& t = T()): mem(t) { }
    void Bar() { /* */ }
    T mem;
    // 其他成员
};
template <>
void Foo<int>::Bar() {
    // 进行应用与 int 的处理
}

​ 所以,当我们用 int 之外的任何类型使用 Foo 时,其成员像往常一样进行实例化。当我们用 int 使用 Foo 时,Bar 之外的成员向往常一样进行实例化。如果我们使用 Foo 的 Bar 成员,则会使用我们的特例化版本。

你可能感兴趣的:(C++,primer,5)