C++ 模板进阶

关于更深一步的模板知识的总结来喽~ 

                                           

目录

非类型模板参数

注意 

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

非类型的模板参数必须在编译期就能确认结果

模板的特化

概念

函数模板的特化

类模板特化

扫描二维码关注公众号,回复: 14281033 查看本文章

全特化

偏特化

部分特化

参数更进一步的限制

模板分离编译

什么是分离编译?

模板的分离编译

 解决方法

将声明和定义同时放在xxx.h(推荐)中或者xxx.cpp中

模板定义的位置显式实例化

模板总结 

优点

缺点


非类型模板参数

我们来思考一个问题:模板参数都是类型的不能具体确定的参数吗?(答案:不是)

模板参数分为类型形参非类型形参
类型形参:出现在模板参数列表中,跟在class或者typename之类的参数类型名称。
非类型形参:就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。

我们看一下模板参数为类型参数的栗子:

template<class T>
class Data
{
private:
	T _data;
};
int main()
{
	Data<int> d;
	return 0;
}

我们来编译一下:

这个栗子的模板参数是class T,这个T一开始在编译阶段是不知道类型的。 

我们再来看一下模板参数为非类型参数的栗子:

template<class T, size_t N = 10>
class Data
{
private:
	T _array[N] = { 0 };
	size_t size;
};
int main()
{
	Data<int> d;
	return 0;
}

我们调试一下看看情况:

        我们发现,上面的模板参数中的size_t也可以作为模板参数来使用。而且N在定义数组的时候,通过调试发现的确开辟了N个为T类型的数组的空间。

总结:size_t定义的类型可以当作常量来使用~

我们也可以不用给模板参数缺省值:

举个栗子:

template<class T, size_t N>
class Data
{
private:
	T _array[N] = { 0 };
	size_t size;
};
int main()
{
	Data<int,10> d;
	return 0;
}

我们调试一下看看情况:

我们发现,我们传给N一个整数,N就被赋予了一个常量的属性。可以用来定义数组了。

当模板缺省值给了时候,并且传参时也传参时:

template<class T, size_t N = 5>
class Data
{
private:
	T _array[N] = { 0 };
	size_t size;
};
int main()
{
	Data<int,10> d;
	return 0;
}

我们调试一下看看情况:

注意 

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

在C++语法中目前还不支持浮点数、字符串、类对象为非类型模板参数,但是其他语言可能支持,所以说,C++未来也是有可能更新这类语法的~

我们举个栗子:

template<string str>
class Data
{
private:

};
int main()
{
	return 0;
}

我们编译一下看看是否能通过:

 很显然,目前还不支持。

浮点数呢?举个栗子:

template<float f>
class Data
{
private:

};
int main()
{
	return 0;
}

我们编译一下看看是否能通过:

很显然,这些情况都不成立。 

非类型的模板参数必须在编译期就能确认结果

像size_t、char、int...这些整型是可以作为非类型模板参数。在编译的时候要确定类型,完成模板实例化。

模板的特化

概念

       通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果。(曾经在写priority_queue模拟实现的时候用过一次,不知道老铁们曾记否~)

函数模板的特化

使用特化的场景,举个栗子:

template<class T>
bool IsEqual(const T& x, const T& y)
{
	return x == y;
}
//函数模板的特化  (针对某些类型的特殊化处理)
bool IsEqual(const char* left, const char* right)
{
	return strcmp(left, right) == 0;
}

int main()
{
	cout << IsEqual(1, 2) << endl;

	const char* str1 = "hello world";
	const char* str2 = "hello world";
	cout << IsEqual(str1, str2) << endl;
	return 0;
}

我们运行一下看看结果:

很显然,IsEqual(1, 2):编译器匹配了模板定义的那个。IsEqual(str1, str2):匹配到了下面定义的那个, 因为这个比模板参数更匹配。

注意:

        可能上面的栗子不是特别合理,因为对于str1和str2两个字符串来说,他们如果字符串内容一样,则指向同一块空间,那么两个指针存的地址也是一样的,然后走模板那个的话就按照整型规则进行比较,地址也是整数表示的,那么也就会一样了。

这就好比这个表情包:

                             

        对于这个场景是不是很形象?实际上bug让我们的程序意外的走通了。但是我们要明白,最好还是自己特化一个版本,否则难免会在一些场景下会出现问题。

上面的栗子是在模板初阶中讲过的,今天我们看看进阶中函数模板是怎么特化的:

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

举个栗子:

template<class T>
bool IsEqual(const T& left, const T& right)
{
	return left == right;
}
//函数模板的特化  (针对某些类型的特殊化处理)
template<>
bool IsEqual<const char* const>(const char*const & left, const char*const& right)
{
	return strcmp(left, right) == 0;
}

int main()
{
	cout << IsEqual(1, 2) << endl;

	const char* str1 = "hello world";
	const char* str2 = "hello world";
	cout << IsEqual(str1, str2) << endl;
	return 0;
}

我们运行一下看看结果:

//函数模板的特化  (针对某些类型的特殊化处理)
template<>
bool IsEqual<const char* const>(const char*const & left, const char*const& right) //这个地方每处的两个const都不能省略,否则编译器会出错。
{
    return strcmp(left, right) == 0;

注意:

        一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出。(也就是说,函数模板的特化实际中用的并不多)

1、函数模板不支持半特化(下面将模板类上有说明)

2、template<>的<>不能有参数!(类模板除外)

3、而且特化模板的指明类型在参数列表中都要用到!

5. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误

6、基本函数模板和特化的函数里面参数个数要一致(里面参数类型可以不一样)

一般都直接这么写:

bool IsEqual(const char* left, const char* right)
{
	return strcmp(left, right) == 0;
}

类模板特化

全特化

全特化即是将模板参数列表中所有的参数都确定化。

举个栗子:

template<class T1, class T2>
class Data
{
public:
	Data() { cout << "Data<T1, T2>" << endl; }
private:
	T1 _d1;
	T2 _d2;
};
template<>
class Data<int, char>
{
public:
	Data() { cout << "Data<int, char>" << endl; }
private:
	int _d1;
	char _d2;
};
int main()
{
	Data<int, int> d1;
	Data<int, char> d2;
	return 0;
}

我们运行一下看看d1和d2对象的匹配情况:

template<> 
class Data<int, char> //这个是对上面Data类的特化版本,实际上是对上面那个类的显示实例化,我们把参数全部显示传递,这就是全特化,和全缺省比较类似~
{
public:
    Data() { cout << "Data<int, char>" << endl; }
private:
    int _d1;
    char _d2;
}; 

偏特化

任何针对模版参数进一步进行条件限制设计的特化版本。

部分特化

举个栗子:

template<class T1, class T2>
class Data
{
public:
	Data() { cout << "Data<T1, T2>" << endl; }
private:
	T1 _d1;
	T2 _d2;
};
 //将第二个参数特化为int
template<class T>
class Data<T, int>
{
public:
	Data() { cout << "Data<T, char>" << endl; }
private:
	int _d1;
	T _d2;
};
int main()
{
	Data<char, int> d1;
	Data<int, char> d2;
	return 0;
}

运行一下:

 //在这个地方我们也可以将第一个参数特化为int
template<class T>
class Data<int char> //不过实际上,更习惯于从右往左 传特定类型

//...

参数更进一步的限制

        偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本

举个栗子:

//基础模板,是实现特化版本的基础
template<class T1, class T2>
class Data
{
public:
	Data() { cout << "Data<T1, T2>" << endl; }
private:
	T1 _d1;
	T2 _d2;
};
//两个参数偏特化为指针类型
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;
};
int main()
{
	Data<double, int> d1; // 调用特化的int版本
	Data<int, double> d2; // 调用基础的模板
	Data<int*, int*> d3; // 调用特化的指针版本
	Data<int&, int&> d4(1, 2); // 调用特化的指针版本
	return 0;
}

运行结果:

Data<int&, int&> d4(1, 2):由于引用必须初始化,所以这里是要传参的。

注意:如果需要传引用的话,这时候T&版本必须自己实现,指针的话没必要,这个编译器会自动推导。

这里特化出引用和指针的版本是为了对参数进一步限制。具体有什么用呢?等将来遇到具体场景再来分析~

模板分离编译

什么是分离编译?

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

模板的分离编译

假如以下场景,模板的声明和定义分开,头文件中进行声明,源文件中完成定义:

举个栗子:

// a.h
template<class T>
T Add(const T& left, const T& right);
// a.cpp
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
// test.cpp
#include"a.h"
int main()
{
    Add(1, 2);
    Add(1.0, 2.0);
    return 0;
}

我们运行一下看看是否能通过编译:

为什么模板声明和定义分离就编译不通过呢?

我们先来分析以一下一个多温江的项目生成可执行文件的过程:

Linux系统下:

通过上面过程就可以发现,

->在a.h中是拿到了要实例化的模板类型,但是这个地方没有定义,也就没有地址。 

->在a.cpp中有对应的函数的定义,但是没有找到要实例化出结果的类型!因为在链接前他们的工作是互不干扰的!

->在链接的时候调用函数的地方就找不到地址!因为函数就没有实例化出来!这个时候寻址就会出错。

总结:模板分离声明定义编译是不会报错,但是链接时寻址就会出错!

我们再来看一下这个过程:

 

 解决方法

将声明和定义同时放在xxx.h(推荐)中或者xxx.cpp中

举个栗子:

//声明
template<class T>
T Add(const T& left, const T& right);
//定义
template<class T>
T Add(const T& left, const T& right)
{
    return left + right;
}
int main()
{
    cout << Add(1, 2) << endl;
    cout << Add(1.1, 2.0) << endl;
    return 0;
}

运行结果:

模板定义的位置显式实例化

这种方法很不实用,不推荐~

举个栗子:

// a.h
template<class T>
T Add(const T& left, const T& right);

// a.cpp
template<class T>
T Add(const T& left, const T& right)
{
	return left + right;
}
template<>
int Add<int>(const int& left, const int& right)
{
	return left + right;
}
template<>
double Add<double>(const double& left, const double& right)
{
	return left + right;
}

// test.cpp
#include"a.h"
int main()
{
    cout << Add(1, 2) << endl;
    cout << Add(1.1, 2.0) << endl;
    return 0;
}

 运行结果:

模板总结 

优点

1. 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
2. 增强了代码的灵活性

缺点

1. 模板会导致代码膨胀问题,也会导致编译时间变长(这个问题对于现在的计算机基本可以忽略了)
2. 出现模板编译错误时,错误信息非常凌乱,不易定位错误

虽然现在是放假期间,但是学习人从不休息!

看到这里给博主支持一下吧~

猜你喜欢

转载自blog.csdn.net/qq_58724706/article/details/124550130