C++之函数模板(还差拓展阅读)

时间:2024-10-22 20:49:56

        在编译器编译阶段,函数会根据调用处的实参类型进行推演,推演生成对应类型的函数以供调用,下面以一张图来展示这一思想

        对于中间这个模版而言,实参调用的时候要保证两个实参的类型相同。如果编译器发现 a 和 b 这两个实参在传入时没有明确指定为相同的类型,那么编译器将会报错。

函数模板的实例化

        用不同类型的参数使用函数模板时,称为函数模板的实例化,实例化分为隐式实例化和显示实例化。注意这两种方式区分的是函数调用处的指定类型的方式

隐式实例化

        隐式实例化就如文章一开头图中的那种调用方式一样,直接让编译器按照实参类型生成不同的函数

显示实例化

        显示实例化是在函数调用处指定函数模板中形参的参数类型,如下图所示

template<class T>
T Add(const T& left, const T& right){
    return left + right;
}

int main(){
    int a = 10;
    double b = 20.0;
 
    // 显式实例化
    Add<int>(a, b);
}

模板隐式实例化和显式实例化的例题

下图所示的程序中 Add<double> (a1, a2); 和 Add<int> (d1, d2); 会调用哪一行的函数(记从上到下Add函数的序号分别为 ①,②,③)

        Add<double> (a1, a2); 会调用 T1 Add(T1 left, T2 right) 实例化生成的函数,因为此时调用函数只能通过排除法选择相应的模板,排除法进行的步骤如下:

首先,Add后面跟了double,说明只能通过生成的模板调用函数,

        那么直接排除 ③ 号函数int Add(const int& left, const int& right)

其次,如果对 ① 号模板进行显示实例化,

                那么将生成double Add(const double& left, const double& right),但是实参的类型是

        int,此时想要调用这个生成的函数,就必须进行强制类型转换,此时就需要强调原则

        原则:函数模板在通过显示实例化推导生成函数后,如果实参需要强转才能匹配这个函数,那么原则上就不会调用这个函数。

        有了原则的原因,此时就会排除①号模板显示实例化后的函数

最后,应该选择 T1 Add(T1 left, T2 right); 

        Add<double> (a1, a2);会调用T1 Add(T1 left, T2 right) 实例化生成的函数,原因和上面一样

总的来说,模版推导函数的优先级如下:

        显示实例化 > 参数类型匹配的自定义函数 > 模板 > 参数不匹配需要强转的自定义函数 

函数模板的特化(24/4/10/1h42m开始讲函数模板特化)

        考虑下面的场景:

        我们已经设计了一个函数模版,函数模板实现的功能是:只需要在调用的时候传入两个类型相同的实参(这两个实参有序关系),就可以判断第一个参数是否小于第二个参数。

        现在考虑一个新的需求场景,这个函数模版需要实现的新的功能是:在调用的时候传入两个类型相同的指针,就可以判断第一个指针解引用之后是否小于第二个指针解引用的结果

        比如:我们想要通过模板实例化,传入两个日期类的指针,判断第一个日期是否小于第二个日期,那么此时有两种特化方案。

        假设我们已经实现了比较的代码

template<class T>
bool Less(T left, T right){
	return left < right;
}

特化方案一:

template<>
bool Less<Date*>(Date* left, Date* right){
	return *left < *right;
}

特化方案二:

template<class T>
bool Less(T* left, T* right){
	return *left < *right;
}

也可以不实用模板,直接写一个函数,每次想通过日期类的指针比较两个日期的大小时,总是能调用到这个函数,这个方案本质上是一种函数重载(使用这种方案的情形较多,还可以使用const修饰)

bool Less<Date*>(Date* left, Date* right){
	return *left < *right;
}

为什么不建议把函数模版的定义和声明分开?

        如果把函数模版的定义和声明分到两个文件中,在编译阶段,处于两个不同的文件的程序无法看到对方变量的情况。这就导致了:

        1. 在函数的定义所在的文件中,不知道函数模板的模板参数应该用什么来推导

        2. 在函数的声明所在的文件中,不知道应该怎么实现这个函数。

最终将导致程序出现链接错误

那么将声明和定义分开,能否避免上面的错误?以下面的情况作为说明

在一个文件夹中有两个文件:Array.cpp 和 Array.h,

        对于 Array.cpp,作为源文件,实现了 array 这个类模版中的size函数

namespace ns{
    template<class T, size_t N>
    size_t array<T, N>::size() const{
        T x = 0;
        x += N;
        return _size
    }
    template
	class array<int>;
}

        对于 Array.h,作为头文件,创建了 array 这个类

namespace bit{
	template<class T, size_t N = 10>
	class array{
	public:
		size_t size()const;
	private:
		T _array[N];
		size_t _size = 0;
	};
}

之所以在 Array.cpp 中显示实例化类模版时可以确定写下class array<int>;  是因为在main.cpp中调用时写写下了如下代码:

int main(){
	ns::array<int> a1;
	cout << a1.size() << endl;
	ns::func();
}

显而易见,这个解决方案的缺陷就是:Array.cpp 要预判所有调用处可能出现的类型,

综上,不建议将函数模板的定义和声明分离

更多拓展阅读,请见我的C++面试之模板博客(挖坑!!!!!)