天天看点

C++:19---重载与模板、模板特例化

一、重载与模板

  • 函数模板可以被另一个模板或一个普通非模板函数重载
  • 如果涉及函数模板,则函数匹配规则会有以下的约束:
  • 如果同样好的函数中只有一个是非模板函数,则选择此函数
  • 如果同样好的函数中没有非模板函数,而有多个函数模板,则其中一个模板比其他模板更特例化,则选择此模板
  • 否则,调用有歧义
  • ①对于一个调用,其候选函数包括所有模板实参推断成功的函数模板实例
  • ②候选的函数模板总是可行的,因为模板实参推断会排除任何不可行的模板
  • ③可行函数(模板与非模板)按类型转换(如果对此调用需要的话)来排序。当然,可以用于函数模板调用的类型是非常有限的
  • ④如果恰有一个函数比任何其他函数都更好的匹配,则选择此函数。但是,如果有多个函数提供同样好的匹配,则:

编写重载模板

  • 我们构造一组函数,它们在调试中可能很有用,用来打印相关的信息,两个重载函数定义如下:
  1. //第一版本
  2. //打印任何类型
  3. template<typename T>
  4. string debug_rep(const T &t)
  5. {
  6. ostringstream ret;
  7. ret << t;
  8. return ret.str();
  9. }
  10. //第二版本
  11. //参数为指针类型的
  12. //注意:此函数不能用于char*(字符指针),因为IO库为char*值定义了一个<<版本,
  13. //此<<版本假定指针表示一个空字符结尾的字符数组,并打印数组的内容而非地址值(我们将在下面介绍如何处理字符指针)
  14. template<typename T>
  15. string debug_rep(T *p)
  16. {
  17. ostringstream ret;
  18. ret << "pointer: " << p; //打印指针自身的值
  19. //如果有内容,打印指针所指的内容
  20. if (p)
  21. ret << " " << debug_rep(*p);//调用上一版本的debug_rep函数
  22. else
  23. ret << " null pointer";
  24. return ret.str();
  25. }
  • 如果我们编写下面的代码:
  • 将调用第一版本的debug_rep。因为第二个版本debug_rep版本要求一个指针参数,所以不符合
  1. std::string s("hi");
  2. std::cout << debug_rep(s) << std::endl;
  •  如果我们编写下面的代码,那么两个函数都可生成实例:
  • debug_rep(const string*&):由第一个版本的debu_rep实例化而来
  • debug_rep(string*):由第二个版本的debu_rep实例化而来
  • 但是第二个版本是最精确的匹配,因为第一个版本需要进行普通指针到const指针的转换。因此编译器调用的是第二个版本
  1. std::string s("hi");
  2. std::cout << debug_rep(&s) << std::endl;

多个可行模板的最终精确选择

  • 根据上面定义的debug_rep(),我们定义下面的调用代码:
  1. std::string s("hi");
  2. const std::string *sp = &s;
  3. std::cout << debug_rep(sp) << std::endl;
  • 此例中的两个模板都是可行的,而且都是精确匹配:
  • debug_rep(const string*&):由第一个版本debu_rep实例化而来
  • debug_rep(const string*):由第二个版本的debu_rep实例化而来
  • 但是根据重载函数模板的特殊规则,此调用的解析被解析为debug_rep(T*),因此调用的是第二个版本的debu_rep
  • 原因在于:debug_rep(const T&)本质上可以用于任何类型(包括指针类型),debug_rep(T*)只适用于指针类型,因此第二版本更适合
  • 当有多个重载模板对一个调用提供同样好的匹配时,应选择最特例化的版本。

非模板和模板的重载

  • 现在我们编写一个非模板版本的debug_rep()函数
  1. //第三版本
  2. //打印双引号包围的string
  3. string debug_rep(const string &s)
  4. {
  5. return '"' + s + '"';
  6. }
  • 现在我们有下面的调用,那么也将有两个版本的函数可调用:
  • debug_rep<string>(const string&):第一版本的模板
  • debug_Rep(const string&):第三版本的普通非模板函数
  • 但是编译器最终选择第三版本来调用。因此当存在多个同样好的函数模板时,编译器选择最特例化的版本,一个非模板函数比一个函数模板更好
  1. std::string s("hi");
  2. std::cout << debug_rep(s) << std::endl;

重载模板和类型转换(处理C风格字符串和字符串字面常量)

  • 现在我们来讨论一下:C风格字符串指针和字符串字面常量
  • 现在我们有下面的调用,那么上面三个版本都可以调用:
  • debug_rep(const T&):T被绑定到char[10](第一版本)
  • debug_rep(T*):T被绑定到const char(第二版本)
  • debug_rep(const string&):要求从const char*到string的类型转换(第三版本)
  • 但是编译器最终选择第二版本来调用。因为第三版本需要进行一次用户定义的类型转换,第一版本不是针对于指针的,第二版本是针对于指针的,因此最终选择第二版本的哈数来调用
std::cout << debug_rep("hi world!") << std::endl; //最终调用debug_rep(T*)
  • 如果希望字符指针按string来处理,可以定义下面两个非模板重载版本:
  1. //将字符指针转换为string,并调用string版本的debug_rep
  2. string debug_rep(char *p)
  3. {
  4. return debug_rep(string(p)); //调用第三版本的
  5. }
  6. string debug_rep(const char *p)
  7. {
  8. return debug_rep(string(p)); //调用第三版本的
  9. }

缺少声明可能导致程序行为异常

  • 我们以上面的使char*版本的debug_rep()的函数为例:
  • 为了使char*版本的debug_rep()可以正常工作,在定义此版本之前,debug_rep(const string&)的声明必须在作用域中
  • 否则,char*版本的debug_rep()就会去调用函数模板的debug_rep(),与我们最初的目的相反了
  1. template<typename T> string debug_rep(const T &t);
  2. template<typename T> string debug_rep(T *p);
  3. //为了使debug_rep(char*)的定义正常工作,此debug_rep()的声明必须在作用域中
  4. //否则debug_rep(char*)将调用模板函数版本的
  5. string debug_rep(const string &s);
  6. string debug_rep(char *p)
  7. {
  8. //如果接收一个const string&的版本的声明不在作用域中
  9. //返回语句将调用debug_rep(const T&)的T实例化为string的版本
  10. return debug_rep(string(p));
  11. }
  • 通常,如果使用了一个忘记声明的函数,代码将编译失败,但对于重载函数模板的函数而言,则不是这样。如果编译器可以从模板实例化出与调用匹配的版本,则缺少的声明就不重要了。在本例中声明接受的string参数的debug_rep版本,编译器会默认地实例化接受const T&的模板版本

二、模板实例化

  • 编写单一模板,使之对任何可能的模板实参都是最适合的,都能实例化,这并不总是能办到。在某些情况下,通用模板的定义对特定类型是不适合的:通用定义可能编译失败或做得不正确。因此我们需对针对类或函数定义一个特例化版本
  • 下面是两个模板:
  1. //第一版本:可以比较任意两个类型
  2. template<typename T>
  3. int compare(const T&, const T&);
  4. //第二版本:处理字符串字面常量
  5. template<size_t N,size_t M>
  6. int compare(const char(&)[N], const char(&)[M]);
  • 只有我们传递给compare资格字符串字面常量或者一个数组时,编译器才会调用第二个版本;如果我们传递给它字符指针,就会调用第一个版本:
  1. const char *p1 = "hi", *p2 = "mom";
  2. compare(p1, p2); //调用第一版本
  3. compare("hi", "mom"); //调用第二版本
  • 我们无法将一个指针转换为一个数组的引用,所以对于p1和p2的使用,调用的是第一版本的模板函数

定义函数模板特例化

  • 为了处理字符指针(而不是数组),可以为第一个版本的compare定义一个模板特例化版本。一个特例化版本就是模板的一个独立的定义,在其中一个或多个模板参数被指定为特定的类型 
  • 特例化一个函数模板时,必须为原模板中的每个模板参数都提供实参。为了指出我们正在实例化一个模板,应使用关键字template后跟一个空尖括号对<>
  1. template<typename T>
  2. int compare(const T&, const T&);
  3. template<size_t N,size_t M>
  4. int compare(const char(&)[N], const char(&)[M]);
  5. //compare的特例化版本,处理字符数组的指针
  6. template<>
  7. int compare(const char* const &p1,const char* const &p2)
  8. {
  9. return strcmp(p1, p2);
  10. }
  • 当我们特例化一个模板时,函数参数类型必须与一个先前声明的模板中对应的类型匹配。本例中我们特例化的模板是:
  1. template<typename T>
  2. int compare(const T&, const T&);
  • 因为我们想要字符指针,因此T为char*,所以最基本的参数应该为const char*&,另外,我们希望定义一个常量指针,所以在char*后面也加一个const

函数重载与模板特例化

  • 当定义函数模板的特例化版本时,我们本质上接管了编译器的工作。即,我们为原模板的一个特殊实例提供了定义。重要的是要弄清楚:一个特例化版本本质上是一个实例,而非函数名的一个重载版本
  • 特例化的本质是实例化一个模板,而非重载它。因此,特例化不影响函数匹配。
  • 但是如果我们将一个特殊的函数定义为一个特例化版本还是一个独立的非模板函数,会影响到函数匹配(例如我们在上面在上面定义的3个compare函数,其中两个是模板,一个是非模板,那么非模板的将与模板函数构成重载)

类模板特例化

  • 除了特例化函数模板,我们还可以特例化类模板
  • 作为了一个例子:
  • 一个重载的调用运算符,它接受一个容器关键字类型的对象,返回一个size_t
  • 两个类型成员,result_type和argument_type,分别调用运算符的返回类型和参数类型
  • 默认构造函数和拷贝赋值运算符
  • 我们将标准库的hash模板定义一个特例化版本,使其来保存我们自定义的Sales_data类
  • 默认情况下,无序容器使用hash<key_type>来组织元素。为了让我们自己的数据类型也能使用这种默认组织方式,我们自定义了一个hash模板的特例化
  • 一个特例化的hash类必须定义:
  • 另外,由于hash模板定义在std命名空间内,所以如果我们想要特例化hash,必须先打开std命名空间,然后在其中进行特例化
  • 下面的代码是针对于hash模板的特例化,其特例化的对象是我们自定义的Sales_data对象,其中有一些注意点:
  • 使用“template<>”表明这是一个特例化版本的类型
  • operator()函数:是用来返回给定类型的值的一个哈希函数。对于一个给定值,任何时候调用此函数都应该返回相同的结果,一个好的哈希函数对不相等的对象(几乎总是)应该产生不同的结果
  • 标准库被内置类型和很多标准库类型都定义了hash类的特例化版本。因此我们在operator()函数中直接调用这些特例化的hash类,然后求取哈希值,最后将哈希值进行按位与(^),最终将哈希结果返回
  1. namespace std {
  2. template<>
  3. struct hash<Sales_data>
  4. {
  5. typedef size_t result_type;
  6. typedef Sales_data argument_type;
  7. size_t operator()(const Sales_data& s)const;
  8. };
  9. size_t hash<Sales_data>::operator()(const Sales_data& s)const
  10. {
  11. //下面的hash类都是标准库针对特定的数据类型进行的特例化,我们直接调用就可以了
  12. return hash<std::string>()(s.bookNo) ^
  13. hash<unsigned>()(s.units_sold) ^
  14. hash<double >()(s.revenue);
  15. }
  16. }
  • 下面是Sales_data类型的定义,由于hash的特例版需要访问Sales_data的私有成员,所以在Sales_data的定义中,我们将hash的特例化版本作为其友元类:
  1. template<class T> class std::hash; //友元声明
  2. class Sales_data {
  3. private:
  4. std::string bookNo;
  5. unsigned units_sold;
  6. double revenue;
  7. friend class std::hash<Sales_data>; //特例化版本的hash为其友元
  8. };
  • 需要注意的是:我们特例化hash类中operator()函数中使用ash()函数计算所有三个数据成员的哈希值,从而与我们为Sales_data定义的operator==是兼容的。默认情况下,为了处理特定关键字类型,无序容器会组合使用key_type对应的特例化hash版本和key_type上的相等运算符
  • 假定我们的特例化版本在作用域中,当将Sales_data作为容器的关键字类型时,编译器就会自动使用上面我们定义的特例化版本,例如:
  1. //使用hash<Sales_data>和Sales_data的operator==
  2. unordered_multiset<Sales_data> SDset;

为了让Sales data的用户能使用hash的特例化版本,我们应该在Sales_ data 的头

文件中定义该特例化版本。

类模板部分特例化

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

我们只能部分特例化类模板,而不能部分特例化函数模板。

  • 例如标准库remove_reference类型,该模板是通过一系列的特例化版本来完成其功能的。定义如下:
  • 第一个模板是最通用的模板,可用于任意类型实例化
  • 第二个模板和第三个模板是特例化版本:根据规则,首先定义模板参数;在类名之后,为要特例化的模板参数指定实参,这些实参列于模板名之后的尖括号中。这些实参与原始模板中的参数按位置对应
  1. //原始的、最通用的版本
  2. template<class T>
  3. struct remove_reference {
  4. typedef T type;
  5. };
  6. //部分特例化版本
  7. template<class T>
  8. struct remove_reference<T&> { //针对于左值引用的
  9. typedef T type;
  10. };
  11. template<class T>
  12. struct remove_reference<T&&> { //针对于右值引用的
  13. typedef T type;
  14. };
  • 当我们有下面的程序时,程序会根据类型自动调用不同的模板
  1. int i;
  2. //调用原始模板
  3. remove_reference<decltype(42)>::type a;
  4. //i为左值引用,调用第一个(T&)部分特例化版本
  5. remove_reference<decltype(i)>::type b;
  6. //std::move(i)返回右值,调用第二个(T&&)部分特例化版本
  7. remove_reference<decltype(std::move(i))>::type c;
  8. //a、b、c均为int

特例化成员而不是类

  • 我们可以之特例化特定成员函数而不是特例化整个模板
  • 例如,如果Foo是一个模板,包含一个成员Bar,我们可以只特例化该成员:
  1. //下面是一个模板类
  2. template<typename T>
  3. struct Foo {
  4. Foo(const T &t = T()) :mem(t) {}
  5. void Bar() {
  6. //通用的Bar()函数
  7. }
  8. T mem;
  9. };
  10. //特例化Foo<int>版本的的成员Bar
  11. template<>
  12. void Foo<int>::Bar()
  13. {
  14. //...
  15. }
  • 我们有下面的调用代码:
  1. Foo<string> fs; //实例化Foo<string>::Foo()
  2. fs.Bar(); //实例化Foo<string>::Bar()
  3. Foo<int> fi; //实例化Foo<int>::Foo()
  4. fi.Bar(); //实例化Foo<int>::Bar()
  • 除了int之外的任何类型都使用在Foo内部定义的Bar()函数,而int类型的Foo对象使用在外部定义的特例化Bar()成员函数 

继续阅读