C++模板进阶

Posted Booksort

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++模板进阶相关的知识,希望对你有一定的参考价值。

非类型模板参数

模板参数

  1. 类型模板参数:用typename/class定义的参数类型名称
  2. 非类型模板参数:用一个整形作为模板的一个参数,在模板中可以当常量使用

例如:

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提供了第三代具体化标准

  1. 对于给定的函数名,可以有非模板函数模板函数显示具体化模板函数以及他们的重制版本
  2. 显示具体化的原型和定义应以template<>,并通过名称来指出类型
  3. 具体化优先于常规模板实例化,而非模板函数优先于具体化常规模板

举个例子:
下面函数分别是常规函数模板函数模板具体化非函数模板函数

//常规模板
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++为了处理这些问题,定义了一个良好的策略,尤其是处理多个参数时,这个过程是重载解析

  1. 创建候选函数列表,其中包含与被调用函数的名称相同的函数与模板
  2. 使用候选函数列表去创建可行函数列表。这些都是参数数目正确的函数,为此有一个隐式转换序列,其中包括实参类型与形参类型完全匹配的情况(如类型之间的转换,例:float转向double,但是模板也可以生成一个float类型的实例)
  3. 确定是否有最佳的可行函数,如果有就是用,否该函数调用就报错

在创建可行函数列表中有隐式转换序列,有些事不能转换的,如:整形不能转换为指针类型

列出可行函数列表后,再开始确定哪个可行函数是最佳的

  1. 完全匹配(常规函数优先于模板)
  2. 提升转换(小类型可以向大类型提升转换,例:char/short转向int,float转向double)
  3. 标准转换(例:Int转换为char,long转换为double)
  4. 用户定义的转换(在调用时,函数名<>,其中指定或不指定类型,让模板去实例化)

总结一下,重载解析将寻找最佳匹配函数。
如果只存在一个这样的函数,则选择它。

如果存在多个这样的函数,但其中只有一个非模板函数(其实就是一个模板实例化的普通函数的重载函数(参数类型不一样,定义也可能不一样,算是具体化的一种)),则选择该函数。

如果函数的调用是不确定的(编译器不知道调用哪个),那么将报错。

针对类模板的特化

与函数特化类似,在特殊场景下,无法满足特殊需求,就需要自己指定。

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代码翻译成汇编。

汇编会把各个文件中的函数汇总,形成符号表。
在编译器中处理的过程:预处理、编译、汇编,都没什么问题

但是在最后的链接器中,出问题了,由于在编译的时候,根据命名修饰规则 ,函数被修饰,但是在汇编中并没有其定义的地址,其地址一直处于未知状态。在最后的链接中,虽然,对于函数模板的定义所处的文件,也会被编译预处理,汇编之列的步骤,但是,这还是处于模板的状态,也就是说,模板并没有针对调用的类型参数进行实例化,所以,根本就没有相关的函数形成符号表,当然也就找不到来链接了。只要有声明就能通过编译,但是最后的链接,是不可找到其相关的函数的定义。所以不可能让模板进行分离编译。

最后就总结,
定义的地方美没有实例化
实例化的地方没有定义,只有声明

所以模板不能分离编译

解决方案

  1. 显式实例化:在定义的地方指定实例化的类型 template void fuc<int>(int a,int b)(这样就能让定义的位置进行实例化,就能形成相应都符号表,能找到地址,可以进行链接)
  2. 不要分离编译(朴实无华)

以上是关于C++模板进阶的主要内容,如果未能解决你的问题,请参考以下文章

C++初阶第十三篇—模板进阶(非类型模板参数+模板特化+模板的分离编译)

C++之模板进阶

C++之模板进阶

C++——模板进阶

C++模板进阶

C++模板进阶