【C++】模板进阶

1.非类型模板参数

  • 类型模板参数:出现在模板参数列表中,跟在class或者typename后面的类型名称
  • 非类型模板参数:使用一个常量来作为类(函数)模板的一个参数,在类(函数)模板中可将参数当成常量来使用

非类型模板参数代码演示

template<class T, size_t N = 10>
    class array
    {
        public:
        T& operator[](size_t index){return _array[index];}
        const T& operator[](size_t index)const{return _array[index];}
        size_t size()const{return _size;}
        bool empty()const{return 0 == _size;}
        
        private:
        T _array[N];
        size_t _size;
    }

注意

浮点数类对象以及字符串不允许作为非类型模板参数

② 非类型模板参数必须在编译期就能确认其值

2. 模板的特化

  • 使用模板可以实现一些与类型无关的代码,但是对于一些特殊的场景,无法处理或者处理时会产生错误
  • 模板的特化:在原模板类的基础上,针对特殊类型进行特殊化的实现方式。
  • 模板特化的分类函数模板特化类模板特化

模板不特化可能产生错误结果的代码验证

#include <iostream>
using namespace std;
template<class T>
bool Max(T& left, T& right)
{
	return left > right;
}

void Test()
{
	char* p1 = "world";
	char* p2 = "hello";
	if (Max(p1, p2))
		cout << p1 << endl;
	else
		cout << p2 << endl;
}
int main()
{
	Test();
	system("pause");
	return 0;
}

输出结果:hello

正常情况下应该输出world,但是却输出了hello,说明该模板对于char*类型失效了

2.1 函数模板特化

函数模板特化步骤

  1. 必须要先有一个基础的函数模板
  2. 关键字template后面接一对空的尖括号<>
  3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型
  4. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误

函数模板特化 代码演示

#include <iostream>
using namespace std;
template<class T>
bool Max(T& left, T& right)
{
	return left > right;
}

// 函数模板特化
template<>
bool Max<char*>(char*& left, char*& right)
{
	if (strcmp(left, right) > 0)
		return true;
	return false;
}

void Test()
{
	char* p1 = "world";
	char* p2 = "hello";
	if (Max(p1, p2))
		cout << p1 << endl;
	else
		cout << p2 << endl;
}
int main()
{
	Test();
	system("pause");
	return 0;
}

输出结果:world

解释:Test函数中调用的Max函数,此处的Max函数走的是特化的逻辑,这样就处理了特殊类型失效的问题

也可以使用该类型的函数取代函数模板特化

bool Max(char*& left, char*& right)
{
	if (strcmp(left, right) > 0)
		return true;
	return false;
}

2.2 类模板特化

2.2.1 全特化

  • 全特化:将模板参数列表全部参数都指定为具体的类型

全特化 代码演示

template<class T1, class T2>	// 1
    class Data
    {
        public:
        Data() {cout<<"Data<T1, T2>" <<endl;}
        private:
        T1 _d1;
        T2 _d2;
    };

template<> 		//2
class Data<int, char>
{
    public:
    Data() {cout<<"Data<int, char>" <<endl;}
    private:
    T1 _d1;
    T2 _d2;
};

void Test()
{
    Data<int, int> d1;	//1
    Data<int, char> d2;	//2
}

2.2.2 偏特化

  • 偏特化:将模板参数列表部分参数指定为具体的类型

偏特化 代码演示

template<class T1, class T2>	// 1
class Data
{
    public:
    Data() {cout<<"Data<T1, T2>" <<endl;}
    private:
    T1 _d1;
    T2 _d2;
};

// 将第二个参数特化为int
template <class T1>		// 2
class Data<T1, int>
{
    public:
    Data() {cout<<"Data<T1, int>" <<endl;}
    private:
    T1 _d1;
    int _d2;
};

void Test()
{
	Data<int, int> d1;	//2
	Data<int, char> d2;	//1
}

偏特化的另一种方式

//两个参数偏特化为指针类型
template <typename T1, typename T2>
class Data <T1*, T2*>
{
    public:
    Data() {cout<<"Data<T1*, T2*>" <<endl;}
    private:
    T1 _d1;
    T2 _d2;
};

//两个参数偏特化为引用类型
template <typename T1, typename T2>
class Data <T1&, T2&>
{
    public:
    Data(const T1& d1, const T2& d2)
        : _d1(d1)
            , _d2(d2)
        {
            cout<<"Data<T1&, T2&>" <<endl;
        }
    private:
    const T1 & _d1;
    const T2 & _d2;
};

void test()
{
    Data<int *, int*> d3; // 调用特化的指针版本
    Data<int&, int&> d4(1, 2); // 调用特化的指针版本
}

2.3 类模板特化应用之类型萃取

  • 可以将内置类型与自定义类型区分开
  • 用TrueType代表内置类型
  • FalseType代表自定义类型
  • 定义TypeTraits,给类型都取别名IsPODType
  • 如果是内置类型则调用对应的类模板特化
  • 如果是自定义得得到F进行浅拷贝Type,通过循环进行赋值深拷贝
#include <string.h>
#pragma warning(disable:4996)
using namespace std;

// 获取该类型属于 内置类型 还是 自定义类型
struct TrueType
{
	static bool Get()
	{
		return true;
	}
};
struct FalseType
{
	static bool Get()
	{
		return false;
	}
};

// 模板特化
template<class T>
struct TypeTraits
{
	typedef FalseType PODTYPE;
};

template<>
struct TypeTraits<int>
{
	typedef TrueType PODTYPE;
};
template<>
struct TypeTraits<double>
{
	typedef TrueType PODTYPE;
};
template<>
struct TypeTraits<short>
{
	typedef TrueType PODTYPE;
};
template<>
struct TypeTraits<float>
{
	typedef TrueType PODTYPE;
};
template<>
struct TypeTraits<long>
{
	typedef TrueType PODTYPE;
};

class String
{
public:
	String(const char* str = "")
	{
		if (nullptr == str)
			str = "";

		_str = new char[strlen(str) + 1];
		strcpy(_str, str);
	}
	String(const String& s)
		: _str(new char[strlen(s._str) + 1])
	{
		strcpy(_str, s._str);
	}
	String& operator=(const String& s)
	{
		if (this != &s)
		{
			char* temp = new char[strlen(s._str) + 1];
			strcpy(temp, s._str);
			delete[] _str;
			_str = temp;
		}

		return *this;
	}

	~String()
	{
		if (_str)
		{
			delete[] _str;
			_str = nullptr;
		}
	}
private:
	char* _str;
};



template<class T>
void Copy(T* dst, const T* src, size_t size, TrueType)
{
	memcpy(dst, src, size*sizeof(T));
}
template<class T>
void Copy(T* dst, const T* src, size_t size, FalseType)
{
	for (size_t i = 0; i < size; ++i)
		dst[i] = src[i];
}
template<class T>
void Copy(T* dst, const T* src, size_t size)
{
	Copy(dst, src, size, TypeTraits<T>::PODTYPE());
}

int main()
{
	int array1[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
	int array2[sizeof(array1) / sizeof(array1[0])];
	Copy(array2, array1, 10);

	String s1[] = { "1111", "2222", "3333" };
	String s2[3];
	Copy(s2, s1, 3);

	return 0;
}

3. 模板分离编译

3.1 什么是分离编译

一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件过程称为分离编译模式

3.1.1 不含模板代码的分离编译

以下三个文件同时存在于VS2013中的一个项目中时,可以正确通过编译链接


//test.h
void f();//这里声明一个函数f


//test.cpp
#include "test.h"
void f()
{
	//TO DO
}  //这里实现出test.h中声明的f函数


//main.cpp
#include "test.h"
int main()
{
	f(); //调用f,f具有外部连接类型
	return 0;
}

原因解释

  • 在这个例子中,test. cpp和main.cpp各自被编译成不同的.obj文件(姑且命名为test.obj和main.obj)
  • 在main.cpp中,调用了f函数,然而当编译器编译main.cpp时,它所仅仅知道的只是main.cpp中所包含的test.h文件中的一个关于void f();的声明,所以,编译器将这里的f看作外部连接类型,即认为它的函数实现代码在另一个.obj文件中,本例也就是test.obj,也就是说,main.obj中实际没有关于f函数的哪怕一行二进制代码,而这些代码实际存在于test.cpp所编译成的test.obj中。
  • 在main.obj中对f的调用只会生成一行call指令,如下
00C7140E  call        f (0C710E6h)  //这里的f的地址就是位于test.obj中的函数地址
  • 在编译时,这个call指令显然是错误的,因为main.obj中并无一行f的实现代码。那怎么办呢?
  • 这就是连接器的任务,连接器负责在其它的.obj中(本例为test.obj)寻找f的实现代码,找到以后将call f这个指令的调用地址换成实际的f的函数进入点地址。
  • 需要注意的是:连接器实际上将工程里的.obj连接”成了一个.exe文件,而它最关键的任务就是上面说的,寻找一个外部连接符号在另一个.obj中的地址,然后替换原来的“虚假”地址。

总结:在不含模板代码的分离编译情形下,如果main源文件中没有某函数的实现,main源文件会寄希望于其他obj文件中去寻找,这个寻找任务就交给了连接器,连接器会去其他obj文件中寻找该函数的实现

3.1.2 含有模板代码的分离编译

由于模板代码,编译器不会将其未被使用到的模板代码实例化,所以找不到函数f的入口地址,所以能通过编译,无法通过连接


//test.h
template<class T>
class A
{
public:
	void f(); // 这里只是个声明
};


//test.cpp
#include "test.h"
template<class T>
void A<T>::f()  // 模板的实现
{
  // TO DO
}


//main.cpp
#include "test.h"
int main()
{
    A<int> a;
    a.f(); 	// #1
    
    A<double> b;
    b.f();	// #2
}

报错

错误	2	error LNK2019: 无法解析的外部符号 "public: void __thiscall A<double>::f(void)" (?f@?$A@N@@QAEXXZ),该符号在函数 _main 中被引用
错误	1	error LNK2019: 无法解析的外部符号 "public: void __thiscall A<int>::f(void)" (?f@?$A@H@@QAEXXZ),该符号在函数 _main 中被引用

原因解释

  • 编译器在**#1#2处并不知道A<int>::f和A<double>::f**的定义,因为它不在test.h里面,于是编译器只好寄希望于连接器,希望它能够在其他.obj里面找到A<int>::f的实例,在本例中就是test.obj,后者中真有A<int>::f的二进制代码吗?
  • NO!!!因为C++标准明确表示,当一个模板不被用到的时侯它就不该被实例化出来,test.cpp中用到了A<int>::f了吗?没有!!所以实际上test.cpp编译出来的test.obj文件中关于A::f一行二进制代码也没有,于是连接器就傻眼了,只好给出一个连接错误。
  • 但是,如果在test.cpp中写一个函数,其中调用A<int>::f,则编译器会将其实例化出来,因为在这个点上(test.cpp中),编译器知道模板的定义,所以能够实例化,于是,test.obj的符号导出表中就有了A<int>::f这个符号的地址,于是连接器就能够完成**#1处的任务,但是连接器依然找不到#2**处的实例化函数(A<double>::f)的地址,因为它未曾在test.cpp中使用过,所以不会由它的实例化函数出现,故A<double>::f这个符号的地址也不会存在

总结:在含有模板代码的分离编译情形下,如果实现和定义分离,且调用与实现也分离,可能会导致模板代码不实例化的情况出现,这就会导致连接出错,无法生成可执行文件

3.2 问题解决方法

  1. 将声明和定义都放到一个".hpp"文件中或者".h"文件中
  2. 模板定义的位置显式实例化

4. 模板总结

【优点】

  1. 模板复用了代码,节约资源,更快的迭代开发
  2. 增强了代码的灵活性

【缺点】

  1. 模板会导致代码膨胀问题,也会导致编译时间变长
  2. 出现模板编译错误时,错误信息很乱,难以定位错误
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值