泛型编程之函数模板和类模板

1. 函数模板

        C++一种编程思想称为泛型编程,主要利用的技术就是模板

        C++提供两种模板机制:函数模板和类模板。这里介绍函数模板,类模板在该专题下的另外篇文章中。

        函数模板作用:建立一个通用函数,其函数返回值类型和形参类型可以不具体指定,用一个虚拟的类来表达。

1.1 函数模板的定义

        函数模板不是一个实在的函数,编译器不能为其生成可执行代码。定义函数模板后只是一个对函数功能框架的描述,当它具体执行时,将根据传递的实际参数决定其功能。

template<class T>
T max(T a, T b)
{
    return a + b;
}

 但是,T可能只是该类型的一部分,例如,如果我们在max函数里面形参声明为const引用

template<typename T>
T max (T const& a, T const& b)
{
    return b < a ? a : b;
}
 
// T是int

在推导的过程中的类型转换需要注意:

        当通过引用声明形参时,即使是平凡的转换也不适用于类型推导。使用相同模板参数T声明的两个形参必须完全匹配。

         当通过值类型声明形参时,仅支持平凡的退化(decay)转换: const和volatile限定符将被忽略;引用转换为引用的类型;原始数据和函数转换为相应的指针类型。使用相同模板参数T声明的两个形参,其退化类型必须匹配。

template<typename T>
T max (T a, T b);

…

int const c = 42;
max(i, c); // OK: T is deduced as int
max(c, c); // OK: T is deduced as int
int& ir = i;
max(i, ir); // OK: T is deduced as int
int arr[4];
max(&i, arr); // OK: T is deduced as int*

max(4, 7.2);  // ERROR: T can be deduced as int or double
std::string s;
max("hello", s); //ERROR: T can be deduced as char const[6] or std::string

 可以通过以下方法解决:

1.强制转换参数,使他们都匹配

2.明确指定(或限定)T的类型,以阻止编译器尝试进行类型推导。

3.指明参数可能有多种不同的类型

max(static_cast<double>(4), 7.2); // OK
max<double>(4, 7.2); // OK

1.2 函数模板的实现原理

  1. 编译器并不是把函数模板处理成能够处理任意类的函数

  2. 编译器从函数模板通过具体类型来产生不同的函数

  3. 编译器会对函数模板进行两次编译

    (1)在声明的位置对模板代码进行编译

    (2)在调用的位置对参数替换后的代码进行编译

 

#include<iostream>
using namespace std;

int     myswap(int  a, int b)
{
    cout << "调用模板函数!!!" << endl;
    return 0;
}

double  myswap(double a, double b)
{
    cout << "调用模板函数!!!" << endl;
    return 0;
}

char myswap(char a, char b)
{
    cout << "调用模板函数!!!" << endl;
    return 0;
}

int main()
{
    myswap(1,1);         
    myswap('a','b');
    myswap(2.0,3.0);        
    
}
#include<iostream>
using namespace std;

template<typename T>
T myswap(T a, T b)
{
  std::operator<<(std::cout, "\350\260\203\347\224\250\346\250\241\346\235\277\345\207\275\346\225\260!!!").operator<<(std::endl);
  return 0;
}


/* First instantiated from: insights.cpp:13 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
int myswap<int>(int a, int b)
{
  std::operator<<(std::cout, "\350\260\203\347\224\250\346\250\241\346\235\277\345\207\275\346\225\260!!!").operator<<(std::endl);
  return 0;
}
#endif


/* First instantiated from: insights.cpp:14 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
char myswap<char>(char a, char b)
{
  std::operator<<(std::cout, "\350\260\203\347\224\250\346\250\241\346\235\277\345\207\275\346\225\260!!!").operator<<(std::endl);
  return 0;
}
#endif


/* First instantiated from: insights.cpp:15 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
double myswap<double>(double a, double b)
{
  std::operator<<(std::cout, "\350\260\203\347\224\250\346\250\241\346\235\277\345\207\275\346\225\260!!!").operator<<(std::endl);
  return 0;
}
#endif


int main()
{
  myswap(1, 1);
  myswap('a', 'b');
  myswap(2.0, 3.0);
  return 0;
}

1.3 函数模板的重载

        所谓的函数模板的重载是指,普通函数的版本函数模板的版本函数模板特例化的版本可以共存,例如:

//普通函数版本
bool Compare(char* a, char* b)
{
	cout << "普通函数版本" << endl;
	return strcmp(a, b) > 0;
}
 
//函数模板版本
template<typename T>
bool Compare(T a, T b)
{
	cout << "函数模板版本" << endl;
	return a > b;
}
 
//模板特例化版本
template<>
bool Compare(char* a, char* b)
{
	cout << "模板特例化版本" << endl;
	return strcmp(a, b) > 0;
}

        调用的顺序:普通函数版本>模板特例化的版本>模板版本,并且调用时要满足精确匹配的规则。例如:

#include<iostream>
using namespace std;
 
//普通函数版本
bool Compare(char* a, char* b)
{
	cout << "普通函数版本" << endl;
	return strcmp(a, b) > 0;
}
 
//函数模板版本
template<typename T>
bool Compare(T a, T b)
{
	cout << "函数模板版本" << endl;
	return a > b;
}
 
//模板特例化版本
template<>
bool Compare(char* a, char* b)
{
	cout << "模板特例化版本" << endl;
	return strcmp(a, b) > 0;
}
 
int main()
{
	Compare("hello", "world");
 
	return 0;
}

        当在主函数中调用Compare函数比较两个常量字符串"hello"和"world"的大小时,首先会按照调用优先级,先调用普通函数版本,但此时普通函数版本的两个形参的类型为char*,但调用点的形参的类型为const char*,不满足精确匹配的规则,因此会按照调用优先级,接着调用模板特例化版本,但此时模板特例化版本的两个形参的类型为char*,但调用点的形参的类型为const char*,不满足精确匹配的规则,因此会按照调用优先级,来调用函数模板版本,此时系统会用const char*来替换T,来进行实例化,最终进行处理。因此这里我们调用的是函数模板。

1.4 函数模板的参数默认值

template <typename T1, typename T2 = int>
class DefClass1 {};

template <typename T1 = int, typename T2>
class DefClass2 {};  // ERROR: 无法通过编译:因为模板参数的默认值没有遵循“由右往左”的规则

template <typename T, int i = 0>
class DefClass3 {};

template <int i = 0, typename T>
class DefClass4 {};  // ERROR: 无法通过编译:因为模板参数的默认值没有遵循“由右往左”的规则

template <typename T1 = int, typename T2>
void DefFunc1(T1 a, T2 b) {}; // OK 函数模板不用遵循“由右往左”的规则

template <int i = 0, typename T>
void DefFunc2(T a) {};  // OK 函数模板不用遵循“由右往左”的规则

        可以看到,不按照从右往左定义默认类模板参数的模板类DefClass2和DefClass4都无法通过编译。

        而对于函数模板来说,默认模板参数的位置则比较随意。

        DefFunc1和DefFunc2都为第一个模板参数定义了默认参数,而第二个模板参数的默认值并没有定义,C++11编译器却认为没有问题。

        函数模板的参数推导规则也并不复杂。简单地讲:如果能够从函数实参中推导出类型的话,那么默认模板参数就不会被使用,反之,默认模板参数则可能会被使用。

template <class T, class U = double>
void f(T t = 0, U u = 0) {};
void g()
{
    f(1, 'c'); // f<int, char>(1, 'c')
    f(1);      // f<int, double>(1, 0), 使用了默认模板参数double
    f();       // 错误: T无法被推导出来
    f<int>();  // f<int, double>(0, 0), 使用了默认模板参数double
    f<int, char>(); // f<int, char>(0, 0)
}

 

定义了一个函数模板f,f同时使用了默认模板参数和默认函数参数。

可以看到,由于函数的模板参数可以由函数的实参推导而出:

在f(1)这个函数调用中,实例化出了模板函数的调用应该为f<int, double>(1, 0),其中,第二个类型参数U使用了默认的模板类型参数double,而函数实参则为默认值0。

类似地,f<int>()实例化出的模板函数第二参数类型为double,值为0。

而表达式f()由于第一类型参数T的无法推导,从而导致了编译的失败。

而通过这个例子也可以看到,默认模板参数通常是需要跟默认函数参数一起使用的。

还有一点应该注意:模板函数的默认形参值不是模板参数推导的依据。函数模板参数的选择,终究是由函数的实参推导而来的。

1.5 函数模板的泛化和特化

        泛化是指模版中的类型都未定,可以支持所有的类型传入。

        全特化是指在泛化之后,对类型输入的时候,指定特定类型,进行定义。

        偏特化是指将指定类型或者指定数量的输入固定,进行定义。

template<class T> // 完全泛化
void fun(T a)
{
}
template<class T> // 部分特化
void fun(T* a)
{
}
template<>
void fun<char*>(char*) // 完全特化
{
}

template<class T, class T1> // 完全泛化
void fun(T a, T1 b)
{
}

template<class T, class T1 = int> // 部分泛化
void fun(T a, T1 b)
{
}

template<>
void fun<char*, int>(char*, int) // 完全特化
{
}

 

重点总结

类模板和函数模板都可以被全特化;
类模板能偏特化,不能被重载;
函数模板全特化,不能被偏特化。
模板类调用优先级

对主版本模板类、全特化类、偏特化类的调用优先级从高到低进行排序是:
全特化类>偏特化类>主版本模板类
这样的优先级顺序对性能也是最好的

面试tag:

(请问函数模板和类模板有什么区别?)
除了上面所说的以外:

    函数模板的实例化是由编译程序在处理函数调用时自动完成的,而类模板的实例化必须由程序员在程序中显式地指定。
    即函数模板允许隐式调用和显式调用而类模板只能显式调用
    与函数模板不同的是,类模板在实例化时必须显式地指明数据类型,编译器不能根据给定的数据推演出数据类型。
    这期间有涉及到函数模板与模板函数,类模板与模板类的概念(类似于类与类对象的区别)。请看下面例子
    注意:模板类的函数声明和实现必须都在头文件中完成,不能像普通类那样声明在.h文件中实现在.cpp文件中。
    即,模板不支持分离编译 。如下是之前总结的模板为什么不支持分离编译
    https://blog.csdn.net/wyn126/article/details/76733943
    含义是:在定义模板的头文件.h时,模板的成员函数实现也必须写在头文件.h中,而不能像普通的类(class)那样,class的声明(declaration)写在.h文件中,class的定义(definition)写在.cpp文件中
    类模板可以全特化和偏特化,而函数模板不能够偏特化,只能全特化。类模板如果需要一个接收指针的偏特化版本,那么就可以指针偏特化实现;而函数模板不存在偏特化。例如,在STL中需要设计的Iterator Traits,用于提取迭代器类的五种关联类型,而同时要使得算法对指针(理解为退化的迭代器)也能使用,而指针不是一个类,如何获取内部的5个类型呢?这里就用到了类模板类的指针偏特化来区分开T的指针 和T 的迭代器。

? 函数模板为什么不能偏特化

?模板函数/函数模板  模板类/类模板 区别

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

林家小院

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值