目录
- 泛型编程
- 模板的使用
- 函数模板
- 函数模板的本质
- 函数模板的实例化
- 显式实例化
- 隐式实例化
- 函数模板的模板参数的匹配原则
- 类模板
- 类模板的本质
- 类模板的实例化
- 非类型模板参数
- 模板特化
- 函数模板特化
- 类模板特化
- 类模板全特化
- 类模板偏特化(半特化)
- 模板分离编译
- typename与class的小区别
泛型编程
提到模板就不得不提泛型编程的概念,泛型编程即编写一段与类型无关的通用代码,使得各种类型的参数都可以复用这段代码,从而顺利偷懒,提升编程效率。举例来说,假如我想实现一个交换函数(swap),那么问题来了,我想让这个函数既可以交换int类型的,也能交换double类型的,怎么处理呢,我们其实可以使用函数重载,实现两个swap函数,一个参数类型是int,一个是double就行,到这里还算轻松,但倘若是三种类型呢,又或者是四种,五种甚至是任意一种,使用函数重载固然可行,但写起来会非常麻烦。这种时候就应该使用泛型编程的思想,利用模板写出一段与类型无关的交换函数的代码,从而做到任意一种类型都能使用这个交换函数。就像给出了一个模子,浇注什么材料就是什么材料的模型。
模板的使用
说了那么多,模板到底应该怎样去使用呢,c++中的模板分为函数模板和类模板,下面我来一一介绍。
函数模板
template<class T>
T func(T a, T b)
{return a + b;
}int main()
{int a = 0;int b = 1;std::cout << func(a, b);return 0;
}
这是函数模板的一个简单的使用展示,template<class T1, class T2, ……, class Tn>的作用是声明模板参数列表,模板参数可以申请任意多个(模板参数一般是莫种类型,当然还有特例,这里我们之后再说),声明完模板参数列表之后,其下方的函数(仅指下方这一个)或类会与具体类型解耦,编译器会根据实际调用时传入的类型,自动生成对应的代码。这时我们就可以使用之前声明过的模板参数替换某些变量的类型,使其可以随模板参数的变化而变化。
函数模板的本质
当时我们将一个函数变成函数模板时,这个函数模板就不再是个函数了,它代表了一个函数家族,它可以通过模板初始化成很多个函数,它本身并不是函数,而是像一张设计图纸,只有传入模板参数时才会初始化成对应的函数。初始化函数的工作从由我们一个个手写变成了编译器自动初始化,减少了我们人写的量,提升了编程效率。
函数模板的实例化
函数模板的实例化分为显式实例化和隐式实例化。
显式实例化
显示实例化即直接明确给出函数的模板参数实例化函数使用,具体如下,
template<class T>
T func(T a, T b)
{return a + b;
}int main()
{int a = 0;int b = 1;std::cout << func<int>(a, b);return 0;
}
我这里在函数模板的后面加上(模板函数声明时有几个模板参数,这里就给几个来初始化),表示显式实例化func传入模板参数为int,之后这个函数就变成了只能接受两个int类型参数的函数了,传入别的类型可能就会报错。这里说可能会报错的原因是编译器在匹配参数类型失败之后也是会尝试隐式类型转换的。
隐式实例化
template<class T>
T func(T a, T b)
{return a + b;
}int main()
{int a = 0;int b = 1;std::cout << func(a, b);return 0;
}
隐式实例化就和我最开始所给的例子一样,不用直接声明,而是根据传入的参数的类型来推导模板参数,给了两个int类型的参数,编译器就会根据这两个参数的类型推导出模板参数为int,自动完成初始化。但倘若向上面这个例子一样,函数模板中两个函数参数所用的是一个模板参数,此时我却传入了两个不同类型的参数,会怎么样呢?答案是一定会报错,而对于显式实例化,却是不一定能够会报错,为什么呢?因为显式实例化提前给明模板参数初始化好了,函数已经是一个确定的函数了, 此时给两个不同类型的变量,编译器会尝试隐式类型转换,是有概率只吃警告不报错的;而对于隐式实例化,面对两个变量时函数模板还处于未确定模板参数未实例化的状态,此时给两个类型不一样的参数,编译器无法推导出模板参数,也就无法实例化,所以直接就报错了,这也是隐式实例化要注意的点,要匹配函数模板的模板参数。在匹配了函数模板的模板参数的情况下,使用隐式实例化还是相对来说比较方便的。此外,使用隐式实例化还应该注意编译器仅靠参数能否推断出全部的模板参数从而完成函数的初始化。就像下面这样,
template<class T, class t>
int func(T a, T b)
{t x;return a + b;
}int main()
{int a = 0;int b = 1;std::cout << func(a, b);return 0;
}
尽管传了看起来正确的参数,但还是会报错,因为只靠传入的参数类型只能推断出模板参数T,但没法知道t,这里就只能用显式实例化了。其实这种写法并不常见,我们写函数模板一般都是给参数上模板,很少在里面的变量用模板参数,但还是应该了解。
函数模板的模板参数的匹配原则
int add(int a, int b)
{std::cout << "add(int a, int b)";return a + b;
}template<class T1, class T2>
T1 add(T1 a, T2 b)
{std::cout << "add(T1 a, T2 b)";return a + b;
}int main()
{int a = 0;int b = 1;add(a, b);return 0;
}
在讲解模板参数的匹配原则之前,我先抛出这样一段代码,请问代码的结果是什么?结果是add(int a, int b),在c++中当我们调用的函数有一个函数模板又有一个确定的函数时,编译器会优先考虑确定的函数,有现成的就先吃现成的,但那也是有前提的,就是在选择用现成的函数能行的情况下不选现成的而选函数模板生成也不会生成一个更好的结果,这种情况下会调用现成的。具体来说向上面所给的例子,给的参数都是int类型,完美与现成的函数匹配,这时就不需要用类模板,因为就算调用类模板,你最都也就只能生成一个都是int类型的参数的函数,不会有更好的结果了。那倘若是下面这种情况,
int add(int a, int b)
{std::cout << "add(int a, int b)";return a + b;
}template<class T1>
T1 add(T1 a, T1 b)
{std::cout << "add(T1 a, T2 b)";return a + b;
}int main()
{int a = 0;double b = 1;add(a, b);return 0;
}
这时两个参数一个是int类型,一个是double类型,编译器依然会优先考虑现成的,能不能调用呢?其实是可以的,编译器会使用隐式类型转换将double变成int传给函数,但我们要明白此时的现成函数虽然可以调用但已经不是最优选择了,倘若模板参数可以生成更匹配的最优解函数,就回去用函数模板了,那我们看看函数模板可不可以生成更优解呢?答案是不行的,这里只给了一个模板参数,要求两个参数的类型一致,两个变量的类型不一致,编译器无法推导模板参数,隐式实例化压根就不会成功,更不用说最优解了,所以这里的答案还是add(int a, int b)。那我们再看像下面这种情况,
int add(int a, int b)
{std::cout << "add(int a, int b)";return a + b;
}template<class T1, class T2>
T1 add(T1 a, T2 b)
{std::cout << "add(T1 a, T2 b)";return a + b;
}int main()
{int a = 0;double b = 0;add(a, b);return 0;
}
这里与上面的不同之处就是模板参数给了两个,也就是允许两个参数类型不同,那答案也就显而易见了,是add(T1 a, T2 b),在现成函数无法完美匹配的前提下模板函数又可以生成更优解,这时就会使用模板函数了。
类模板
template<class T>
struct jiunian
{jiunian(const T& x):_a(x){}T _a;
};int main()
{jiunian<std::string> a(std::string("hello world"));std::cout << a._a;return 0;
}
以上就是一个类模板的简单演示,学习过函数模板之后,对于类模板基本就是一看就会,使用template<class T1, class T2, ……, class Tn>声明模板参数列表,使其下方的函数(仅指下方这一个)或类与具体类型解耦,编译器会根据实际调用时传入的类型,自动生成对应的代码。
类模板的本质
与函数模板类似,类模板代表了一个类家族,它不再指一个类,而是一种设计图,传入不同的参数会生成不同的类。
类模板的实例化
类模板不像函数模板,没有隐式实例化和显式实例化之分,因为类没有参数,所以直接传参数实例化就行。
template<class T>
struct a
{T _a;
};int main()
{a<int> x;return 0;
}
非类型模板参数
前面笔者也说过,模板参数通常情况之下是类型,但是c++之后也是更新了特例,如下所示,
template<size_t N>
struct jiunian
{int _a[N] = {0};
};int main()
{jiunian<10> a;std::cout << a._a[9];return 0;
}
c++允许在模板参数中用一个常量作为模板的参数,在模板类(函数)中可以将其作为常量来使用,但只支持整形类型的常量(int,char等),其他的都不支持,其设计的目的很大部分是为了能在类中定义普通数组,因为普通数组只能用常量指定大小,也因此要注意倘若使用非类型模板参数必须确保代码编译时就能确定参数的值,即只能传常量而不能传变量来初始化非类型模板参数。
模板特化
我们在使用模板进行泛型编程时,有时会遇到对于特定的类型不能用模板来处理而需要单独拎出来处理的情况,这是我们就要使用模板特化。
函数模板特化
template<class T>
bool compare(T a, T b)
{return a < b;
}template<>
bool compare<int*>(int* a, int* b)
{return *a < *b;
}int main()
{int a = 1;int b = 2;int* _a = &a;int* _b = &b;std::cout << compare(_a, _b);return 0;
}
一个简单的函数特化的使用就如上所示,笔者对比较函数传入int类型指针时进行了特化,因为指针进行比较是没有意义的,地址在每次运行时都会变化,时大时小,所以对其进行特化,对于int类型指针先引用再比较。但是实际上以上的操作可以直接像之前一样写一个函数重载就行了,编译器会选择更合适的一个,所以函数特化用的其实不是特别的多。
类模板特化
template<class T>
struct compareruler
{bool operator()(const T& a, const T& b){std::cout << "compareruler" << std::endl;return a < b;}
};template<class T>
struct compareruler<T*>
{bool operator()(T* const& a, T* const& b){std::cout << "compareruler<T*>" << std::endl;return *a < *b;}
};int main()
{int a = 0;int b = 1;int* _a = &a;int* _b = &b;compareruler<int*>comp;std::cout << comp(_a, _b);return 0;
}
一个简单的函数特化的使用就如上所示,笔者对于用于比较的仿函数进行了类模板特化,使其对于所有的指针类参数都能先解引用再比较。细心的读者可能发现我这里的类模板特化与之前的不一样,template<>中有参数,这是因为我这里对类模板进行了偏特化,类模板的特化与函数不一样,分为全特化与偏特化,下面一一讲解。
类模板全特化
全特化就是对类模板中的全部参数都确定化,因为类模板中的全部参数都确定了,template<>也就不需要声明模板参数了,这也表示之前的函数模板特化都是全特化,函数模板特化也只支持全特化。类模板全特化具体使用起来就像下面这样,
template<class T>
struct compareruler
{bool operator()(const T& a, const T& b){std::cout << "compareruler" << std::endl;return a < b;}
};template<>
struct compareruler<int*>
{bool operator()(int* const& a, int* const& b){std::cout << "compareruler<int*>" << std::endl;return *a < *b;}
};int main()
{int a = 0;int b = 1;int* _a = &a;int* _b = &b;compareruler<int*>comp;std::cout << comp(_a, _b);return 0;
}
这里只针对int类型指针进行特化,使仿函数传入int类型指针时会先解引用再比较,此时的template<>就不需要写参数了。
类模板偏特化(半特化)
偏特化是针对模板参数进行进一步限制的特化版本,这个限制可以是对参数的一部分进行特化,就像下面这样,
template<class T1, class T2>
struct compareruler
{bool operator()(const T1& a, const T2& b){std::cout << "compareruler" << std::endl;return a < b;}
};template<class T1>
struct compareruler<T1, int>
{bool operator()(const T1& a, const int& b){std::cout << "compareruler<T1, int>" << std::endl;return a > b;}
};int main()
{int a = 0;int b = 1;compareruler<int, int>comp;std::cout << comp(a, b);return 0;
}
也可以是对参数进行进一步的限制,就像开头给的例子一样
template<class T>
struct compareruler
{bool operator()(const T& a, const T& b){std::cout << "compareruler" << std::endl;return a < b;}
};template<class T>
struct compareruler<T*>
{bool operator()(T* const& a, T* const& b){std::cout << "compareruler<T*>" << std::endl;return *a < *b;}
};int main()
{int a = 0;int b = 1;int* _a = &a;int* _b = &b;compareruler<int*>comp;std::cout << comp(_a, _b);return 0;
}
只要是没有完全将类模板确定下来,就算是偏特化,像下面这样既有部分参数特化也有参数进一步限制的,只要不是完全确定,就也算是偏特化。
template<class T1, class T2>
struct compareruler
{bool operator()(const T1& a, const T2& b){std::cout << "compareruler" << std::endl;return a < b;}
};template<class T1>
struct compareruler<T1*, int*>
{bool operator()(T1* const& a, int *const& b){std::cout << "compareruler<T1*, int*>" << std::endl;return *a < *b;}
};int main()
{int a = 0;int b = 1;int* _a = &a;int* _b = &b;compareruler<int*, int*>comp;std::cout << comp(_a, _b);return 0;
}
模板分离编译
这是我们日常写代码时要避免的一个坑,我们再实际编写c++项目时,时常会用到分离编译,即使用多个源文件完成项目,在使用模板时倘若出现函数声明和定义不在一个源文件中就会出现报错,就像下面这样,
//源文件1
template<class T>
void test(T a)
{std::cout << a;
}//源文件2
template<class T>
void test(T a);int main()
{int a = 0;test(a);return 0;
}
编译器会报链接错误,但声明和定义都有,这是为什么呢?因为在c++的代码编译过程中是先分别编译各个源文件中的代码然后将函数连接起来最后在运行的,虽然我们在源文件2显式传参实例化了函数模板,但因为和它同一个源文件的只是一个函数声明,没法实例化,只能等着链接阶段站是否有这个函数,而在真正有函数定义的源文件1却因为源文件分离编译的缘故无法实例化,这就使得test变成了只有声明没有定义的空头函数,链接时就会报链接错误。怎么处理呢?一种方法是直接将声明和定义放在一个源文件之中,另一种就是在函数定义的源文件中显式实例化它,具体如下,
//源文件1
template<class T>
void test(T a)
{std::cout << a;
}template
void test<int>(int a);//显式实例化//源文件2
template<class T>
void test(T a);int main()
{int a = 0;test(a);return 0;
}
template加显式实例化的函数就是强制让编译器将指定的函数模板显式实例化为指定的一份等着链接。
分离编译的情况在类模板中也会出现,比如类模板函数的声明定义分离,如果写在了两份文件中也会引发连接错误,就像下面这样,
//文件1
template<class T>
struct a
{void print(T x);
};int main()
{a<int> A;A.print(1);return 0;
}//文件2
template<class T>
void a<T>::print(T x)
{std::cout << x;
}
这时和函数模板一样,要么写在一份文件中,要么强制显式实例化。
//文件1
template<class T>
struct a
{void print(T x);
};int main()
{a<int> A;A.print(1);return 0;
}//文件2
template<class T>
void a<T>::print(T x)
{std::cout << x;
}template
class a<int>;
typename与class的小区别
笔者在之前模板类型参数声明时都是使用的class,其实声明模板类型参数还可以用typename,老实说其实声明模板类型参数时用typename更为贴切,typename翻译过来就是类型名,更贴切使用场景,但因为class字母数更少,很多人包括我都更喜欢使用class,在声明模板类型参数这个用途上,两者没有任何区别,但对于typename还有一个class无法替代的用法。在引用嵌套依赖类型时必须使用typename明确告诉编译器这是一个类型,什么意思呢?就像下面这样,
template<class T>
class a
{T::iterator a;
};
T::it a;前面若不加typename就会报错,因为这里的it具体类型依赖于T,所以叫嵌套依赖类型。为什么嵌套依赖类型不加typename就会报错呢?因为编译器无法正确识别这是个类型,为什么无法识别呢?因为T::it既可能是一种类型,也可能是一个静态成员变量。假如T是一个类,那it就有可能表示静态成员变量,编译器无法分清,所以就会报错。需要注意,只要这里的T是一个处于不确定状态的类型,也就是模板没有实例化,因为模板没有实例化,编译器就无法在代码中查找,无法查找就无法确定。T可以有各种形式,即使是像
std::vector<T>::iterator a;
这种c++自己库中的容器的情况也不行,因为有模板参数,所以没有实例化,没有实例化就没有代码可以查找,编译器就无法确定。只要是确定的类型,像
std::vector<int>::iterator a;
这样就不用加(当然想加也能加,typename就是告诉编译器这是个类型),当然这时就要确保vector真有iterator这个类型了,不然编译器会直接检查出来,之前给一个不确定的类型加typename可以先把编译器糊弄过去,等到运行时模板实例化了才会找,找不到就是运行时的报错。