非类型模板参数
模板参数
- 类型模板参数:用
typename/class
定义的参数类型名称 - 非类型模板参数:用一个整形作为模板的一个参数,在模板中可以当常量使用
例如:
template<class T,size_t N>//T就是类型模板参数,N就是非类型模板参数
这个N可以直接当常量用,就是说可以直接去初始化数组的元素个数
举个实际点的例子
#include <iostream>
template <class T ,size_t N>
class arr
{
public:
size_t size(void)
{
return sizeof(_a) / sizeof(_a[0]);
}
private:
int _a[N];
};
int main(void)
{
arr<int,100> arr1;
std::cout << arr1.size() << std::endl;
arr<double, 20> arr2;
std::cout << arr2.size() << std::endl;
return 0;
}
我们可以利用这个创建一个可控长度的数组,
非常surprise and comfortable
也算是了了我想用可自选长度的数组的心愿了,虽然C语言中有变长数组,但这个还是要用的舒心一些。
还有最重要的一点:非类型模板参数只能是 int 整形常量
模板参数”缺省“
还有一点特别好玩的,可以给模板 ”赋初值“
继续拿刚刚的例子
#include <iostream>
template <class T=int ,size_t N=10>
class arr
{
public:
size_t size(void)
{
return sizeof(_a) / sizeof(_a[0]);
}
private:
int _a[N];
};
int main(void)
{
arr<> arr1;
std::cout << arr1.size() << std::endl;
arr<double, 20> arr2;
std::cout << arr2.size() << std::endl;
return 0;
}
就是跟函数的参数列表中的缺省参数类似,如果没有传要实例化的参数,就会自动调用,并实例化初值的类。挺好玩的。
模板的特化
对于类模板去实例化具体的类,是编译器去处理,根据给的类型去推出,自动生成的。
不过,在一些特殊场景下,有时候编辑器自动实例化的模板不能正确处理我们所需要的的逻辑,无法满足我们的需求,需要针对一些情况处理进行特殊化处理
举个经典例子,常量字符串与字符串的例子
#include <iostream>
template <class T1,class T2>
bool compare(const T1& left,const T2& right)
{
return left == right;
}
int main(void)
{
char s1[] = "Hello World";
char s2[] = "Hello World";
std::cout << compare(s1, s2)<<std::endl;
const char* s3 = "Hello World";
const char* s4 = "Hello World";
std::cout << compare(s3, s4)<<std::endl;
return 0;
}
看看输出结果:
而作为一个简单问题,其原因在于:
s1,s2都是数组,储存位置在 栈,且储存在不同的位置。
s3,s4是常量字符串,储存位置在静态区。只有这一份数据。
也就是说,s1,s2作为指针,其实指向的是栈区中不同的位置,
而s3,s4也是指针,指向静态区的同一个位置。
而,函数传递过去的参数,被编译器推断成指针也就是地址,函数区比较两个地址是否一样。这就远离我们的期望了。
我们的目的是比较两个字符串是否完全相等。
所以,针对一些特殊要求的函数,需要尽心一些特俗化处理,使之满足我们的需求。
#include <iostream>
//函数模板
template <class T>
bool compare(T left,T right)
{
return left == right;
}
///特化
template<>
bool compare<char*>(char* left, char* right)
{
return std::strcmp(left, right)== 0;
}
template<>
bool compare<const char*>(const char* left, const char* right)
{
return std::strcmp(left, right) == 0;
}
int main(void)
{
char s1[] = "Hello World";
char s2[] = "Hello World";
std::cout << compare(s1, s2)<<std::endl;
const char* s3 = "Hello World";
const char* s4 = "Hello World";
std::cout << compare(s3, s4)<<std::endl;
std::cout << compare(1.1, 1.2) << std::endl;
return 0;
}
显示具体化
为了满足可以在特定的场景满足特定的需求,C++98提供了第三代具体化标准
- 对于给定的函数名,可以有
非模板函数
、模板函数
和显示具体化模板函数
以及他们的重制版本
。 - 显示具体化的原型和定义应以
template<>
,并通过名称来指出类型 - 具体化优先于常规模板实例化,而非模板函数优先于具体化和常规模板
举个例子:
下面函数分别是常规函数模板,函数模板具体化,非函数模板函数
//常规模板
template <class T>
bool compare(T left,T right)
{
return left == right;
}
//具体化的原型
template<>
bool compare<char*>(char* left, char* right)
{
return std::strcmp(left, right)== 0;
}
//非函数模板函数
bool compare(char* left, char* right)
{
return std::strcmp(left, right) == 0;
}
所以说,函数特化,其实就是非模板函数于显示具体化的模板函数
实例化与具体化
为了进一步了解模板,必须理解实例化
与具体化
模板C++提供的特性之一,模板其本身并不会生成函数定义,它只是一个用于生成函数定义的解决方案。
函数模板就像的图纸,函数的定义就是房子,图纸设计好了,并不代表房子也建好了。
在函数被调用时,模板会根据函数调用时的参数来生成指定的函数定义。得到了模板对指定类型的实例化。这就是隐式实例化
还有显示实例化,需要什么类型,就提前声明,这样可以直接命令编辑器生成特定的函数定义。
template bool compare<int>(int l, int r);
需要用<>
来指定类型。
具体化和实例化存在差别,他是为特殊情况下满足特殊需求存在的。
template<> bool compare(char* left, char* right);
template<> bool compare<char*>(char* left, char* right);
这些是显式具体化的声明,但是,提供显示具体化是为了满足特殊需求,所以函数模板的定义有些情况并不能满足时,就可以用具体化来从新定义函数,且必须要有自己的定义,不然为什么要提供这个特性。
注意
嗯,事实上是没有模板函数这个概念的,只有函数模板,而函数模板实例化出来的函数其实就是一个普通的函数。
重载解析
但是,对于编辑器而言,其中的函数,有非函数模板函数,显示具体化,函数模板实例化的常规函数,以及他们的重载,这些不同版本的函数(其函数名一定是一样的)。编辑器该如何选择?
C++为了处理这些问题,定义了一个良好的策略,尤其是处理多个参数时,这个过程是重载解析
- 创建候选函数列表,其中包含与被调用函数的名称相同的函数与模板
- 使用候选函数列表去创建可行函数列表。这些都是参数数目正确的函数,为此有一个隐式转换序列,其中包括实参类型与形参类型完全匹配的情况(如类型之间的转换,例:
float
转向double
,但是模板也可以生成一个float类型的实例) - 确定是否有最佳的可行函数,如果有就是用,否该函数调用就报错
在创建可行函数列表中有隐式转换序列,有些事不能转换的,如:整形不能转换为指针类型
列出可行函数列表后,再开始确定哪个可行函数是最佳的
- 完全匹配(常规函数优先于模板)
- 提升转换(小类型可以向大类型提升转换,例:char/short转向int,float转向double)
- 标准转换(例:Int转换为char,long转换为double)
- 用户定义的转换(在调用时,函数名<>,其中指定或不指定类型,让模板去实例化)
总结一下,重载解析将寻找最佳匹配函数。
如果只存在一个这样的函数,则选择它。
如果存在多个这样的函数,但其中只有一个非模板函数(其实就是一个模板实例化的普通函数的重载函数(参数类型不一样,定义也可能不一样,算是具体化的一种)),则选择该函数。
如果函数的调用是不确定的(编译器不知道调用哪个),那么将报错。
针对类模板的特化
与函数特化类似,在特殊场景下,无法满足特殊需求,就需要自己指定。
template <class T1,class T2>
class Tmp
{
public:
Tmp()
{
cout << "Tmp<T1,T2>" << endl;
}
private:
T1 _a;
T2 _b;
};
这就是类模板,编辑器会根据指定的类型去实例化相应的类。
全特化
类的特化,或者说全特化,我理解为类的具体化(不知道对不对)
这个编辑器会先去匹配已经具体化的类(类型参数要完全符合),就不会去实例化类模板,而是直接调用最佳的类去创建对象。
#include <iostream>
using namespace std;
template <class T1,class T2>
class Tmp
{
public:
Tmp()
{
cout << "Tmp<T1,T2>" << endl;
}
private:
T1 _a;
T2 _b;
};
template <>
class Tmp<int,int>
{
public:
Tmp()
{
cout << "Tmp<int,int>" << endl;
}
private:
int _a;
int _b;
};
int main(void)
{
Tmp<int ,int> t1;
Tmp<double, float> t2;
return 0;
}
这两个暑促结果足以证明两个对象的创建,不是同一个类。
偏特化
也就是说,类也可以只特化一部分。
比如这样
template <class T>
class Tmp<T,int>
{
public:
Tmp()
{
cout << "Tmp<T,int>" << endl;
}
private:
T _a;
int _b;
};
还可以特化为指针或者引用
template <class T1, class T2>
class Tmp<T1*,T2*>
{
public:
Tmp()
{
cout << "Tmp<T1*,T2*>" << endl;
}
private:
T1 _a;
T2 _b;
};
所有实验例子
#include <iostream>
using namespace std;
template <class T1,class T2>
class Tmp
{
public:
Tmp()
{
cout << "Tmp<T1,T2>" << endl;
}
private:
T1 _a;
T2 _b;
};
template <>
class Tmp<int,int>
{
public:
Tmp()
{
cout << "Tmp<int,int>" << endl;
}
private:
int _a;
int _b;
};
template <class T1, class T2>
class Tmp<T1*,T2*>
{
public:
Tmp()
{
cout << "Tmp<T1*,T2*>" << endl;
}
private:
T1 _a;
T2 _b;
};
template <class T>
class Tmp<T,int>
{
public:
Tmp()
{
cout << "Tmp<T,int>" << endl;
}
private:
T _a;
int _b;
};
int main(void)
{
Tmp<int ,int> t1;
Tmp<double, float> t2;
Tmp<double , int> t3;
Tmp<double*, int*> t4;
return 0;
}
输出结果
这样可以更方便的符合使用者的需求,减少限制。
模板分离编译
在我模拟实现一些简单STL类模板时,如:string/vector/list/priority_queue
,我对类模板的成员函数的定义与声明都是放在同一个.h
文件中,本来大型工程中的声明.h
文件与定义.cpp
文件都是分开为声明与定义。但是这里却无法运行,报错原因是找不到链接。 但也就是说,在编译之前的过程,这个工程是可以跑过的。那么我就要从工程执行的过程开始寻找问题。
我们都知道C/C++
的编译过程:
预处理->编译->汇编->链接
test.cpp -> test.s- > test.i -> test.o -> a.out(可执行文件)
这个过程不清楚的可以去看C/C++编译过程
main.cpp
#include <iostream>
using namespace std;
#include "test.h"
int main(void)
{
Data d;
}
test.h
#pragma once
class Data
{
public:
Data();
~Data();
private:
};
test.cpp
#include "test.h"
Data::Data()
{
}
Data::~Data()
{
}
这个是可以跑过的,因为这是一个类,而不是模板。
但是,对于类模板而言,这就无法通过编译。
首先程序要进行 预处理,
预处理要
删除注释
将#include
等头文件内容拷贝到文件中,
宏替换
条件编译
.h
文件会被拷贝到main.c
文件的开头。
再进行 编译
编译
把cpp代码翻译成汇编代码
语法分析(判断代码的语法是否正确)
词法分析
语义分析(翻译每段代码的意思)
符号汇总
编译时,代码编译器检查到使用类模板创建对象后者调用函数模板的函数,由于预处理时,已经包含了类模板或者函数模板的声明,编译器就会认为这个函数已经声明了,就相当于一个通行证,只要这些自定义的东西声明了,就都能通过编译,不管有没有定义,或者找不找得到定义。
能通过编译,语法也没什么问题,语义还是那个也没有什么二义性,编译器就会把cpp代码翻译成汇编。
汇编会把各个文件中的函数汇总,形成符号表。
在编译器中处理的过程:预处理、编译、汇编,都没什么问题
但是在最后的链接器中,出问题了,由于在编译的时候,根据命名修饰规则 ,函数被修饰,但是在汇编中并没有其定义的地址,其地址一直处于未知状态。在最后的链接中,虽然,对于函数模板的定义所处的文件,也会被编译预处理,汇编之列的步骤,但是,这还是处于模板的状态,也就是说,模板并没有针对调用的类型参数进行实例化,所以,根本就没有相关的函数形成符号表,当然也就找不到来链接了。只要有声明就能通过编译,但是最后的链接,是不可找到其相关的函数的定义。所以不可能让模板进行分离编译。
最后就总结,
定义的地方美没有实例化
实例化的地方没有定义,只有声明
所以模板不能分离编译
解决方案
- 显式实例化:在定义的地方指定实例化的类型
template void fuc<int>(int a,int b)
(这样就能让定义的位置进行实例化,就能形成相应都符号表,能找到地址,可以进行链接) - 不要分离编译(朴实无华)