C/C++编程:模板实参

1060 篇文章 297 订阅

模板实参是指:在实例化模板时,用来替换模板参数的值。我们可以使用下面几种不同的机制来确定这些值:

  • 显式模板实参:紧跟在模板名称后面,在一对<>内部的显式模板实参值。所组成的整个实体称为template-id
  • 注入式(injected)类名称:对于具有模板参数P1、P2…的类模板,在它的作用域中,模板名称(即X)等同于template-id:X<P1, P2,...>
  • 缺省模板实参:如果提供缺省模板实参的话,在类模板的实例中就可以省略显式模板实参。然而,即使所有的模板参数都具有缺省值,<>也不能省略
  • 实参演绎:对于不是显式指定的函数模板实参,可以在函数的调用语句中,根据函数调用实参的类型来演绎出函数模板实参。另外,实参演绎还可以在其他几种情况下出现。另外,如果所有的模板实参都可以通过演绎获得,那么在函数模板名称后面就不需要指定<>

函数模板实参

对于函数模板的模板实参,我们可以显式的指定它们,或者借助于模板的使用方式对它们进行实参演绎。比如:

tempplate<typename T>
inline T const& max(T const& a, T const& b){
	return a < b; b : a;
}

int main(){
	max<double>(1.0, 3.0); // 显式的指定模板实参
	max(1.0, -3.0); //模板实参被隐式演绎成double
	max<int>(1.0, 3.0); // 显式的<int>禁止了演绎,因此返回结果是int类型
}

然而,某些模板实参永远也得不到演绎的机会。因此,我们最好把这些实参所对应的参数放在模板参数列表的开始处,从而可以显式的指定这些参数,而其他的参数依旧可以进行实参演绎。比如:

template<typename DstT, typename SrcT>
inline DstT implicit_cast(SrcT const & x){ //SrcT可以被演绎,DstT不可以
	return x;
}

int main(){
	double value = implicit_cast<double>(-1);
}

如果我们调换了例子中模板参数的顺序(即template<typename SrcT, typename DstT>),那么调用implicit_cast就必须显式指定两个模板实参。

由于函数模板可以被重载,所以对于函数模板而言,显式提供所有的实参并不足以标识每一个函数:在一些例子中,它标识的是许多函数组成的函数集合。比如:

template<typename Func, typename T>
void apple(Func func_ptr, T x){
	func_ptr(x);
}

template<typename T> void single(T);

template<typename T> void multi(T);
template<typename T> void multi(T*);

int main(){
	apple(&single<int>, 3);  //正确
	apple(&multi<int>, 7); //错误, multi不唯一
}

第一个apple是正确的,因为表达式&single<int>的类型是确定的,因此可以很容器的就演绎出Func参数的模板实参值。但是,第二个apple中的&multi<int>可以是两种函数类型中的任意一种,因此在这种情况下会产生二义性,不能演绎出Func的实参。

另外,在函数模板中,显式指定模板实参可以会试图构造一个无效的C++类型。考虑下面的重载模板函数:

template<typename T> RT1 test(typename T::X const *);
template<typename T> RT2 test(...);

表达式test<int>可能会使得第1个函数模板无意义,因为基本int类型没有成员类型X。但是对于第2个函数就没有这种问题。因此,表达式&test<int>能够标识一个唯一函数的地址(即第2个函数的地址)。而且,不能用int来替换第一个模板的参数,并不意味着&test<int>是非法的(就是下面的SFINAE原则)。实际上,它是有效合法的。

显然,替换失败并非错误(substitution-failure-is-not-an-error,SFINAE)原则是令函数模板可以重载的重要因素。然而,它同时也涉及到编译器技术。比如,假设类型RT1和RT2的定义如下:

typedef char RT1;
typedef struct {char a[2];} RT2;

于是,我们就可以在编译器检查(也就是说,检查是否可以把它看成一个const-expression)给定类型T是否具备成员类型X:

#define type_has_member_type_x(T) (sizeof(test<T>(0)) == 1)

为了理解宏中的表达式,采取由外至内的分析方法比较简单。首先,对于sizeof表达式,如果选择的是第1个test模板(它返回一个大小为1的char),它将等于1;而另一个test模板会返回一个大小至少为2的结构(因为它包含一个由两个char组成的数组)。换句话说,可以把这个宏看出是一个用来确定const-expression的装置,它可以判断调用test<T>(0)时调用的是哪一个test模板。显然,如果给定的类型T没有成员类型X,那么就不能选择第1个模板。相反,如果T具有成员类型X,那么根据重载解析规则:从0到空指针常量的类型的类型转换要优先于绑定一个实参给省略号参数(省略号参数是最弱的绑定类型),将会调用第1个模板。

SFINAE原则保护的只是:允许试图创建无效的类型,但不允许试图计算无效的表达式。因此,下面的是错误的:

template<int I> void f(int (&)[24/(4-i)]);
template<int I> void f(int (&)[24/(4+i)]);

int main(){
	&f<4>; //错误,替换后第一个除数等于0(不能应用SFINAE)。
	        // 对于第2个是正确的
}

这种错误只会在表达式自身出现,不会在模板参数表达式中出现。因此,下面是合法的:

template<int N> int g(){return N;};
template<int *P> int g(){return *P};

int main(){
	return g<1>(); //虽然1不能绑定到int*参数,但是应用了SFINE原则
}

类型实参

模板的类型实参是一些用来指定模板类型参数的值。我们大多数类型都可以被用作模板的类型实参,但有两种情况例外:

  • 局部类和局部枚举(也即是函数定义内部声明的类型)不能作为模板的类型实参
  • 未命名的class类型或者未命名的枚举类型不能作为模板的类型实参(但是,通过typedef声明给出的未命名类和枚举是可以作为函数类型实参的
template<typename T> clas List{
//	...
};


typedef struct{
	double x, y, z;
}Point;

typedef enum{red, green, blue} *ColorPtr;

int main(){
	struct Assocation{
		int *p;
		int *x;
	};  //局部类

	List<Assocation*> error1; //错误,模板实参中用了局部类型
	List<ColorPtr> error2; //错误:模板实参中用到了未命名的类型(typedef 定义的是*ColorPtr,不是ColorPtr)
	List<Point> ok;  // 正确:通过使用typedef定义的未命名类型
}

通常而言,尽管其他的类型都可以用作模板实参,但前提是该类型替换模板参数之后获得的构造必须是有效的。

template<typename T>
void clear(T p){
	*p = 0;    //要求*可以用于T
}

int main(){
	int a;
	clear(a);  // 错误:int类型不支持*
}

非类型实参

非类型模板实参是那些非类型参数的值。这些值必须是以下几种的一种:

  • 某一个具有正确类型的非类型模板参数
  • 一个编译期整型常值(或者枚举值)。这只有在参数类型和值的类型能够进行匹配,或者值的类型可以隐式转换为参数类型(比如,一个char值可以作为int参数的实参)的前提下,才是合法的
  • 前面有单目运算法&(即取址)的外部名称或者函数的名称。对于函数或者数组变量,&运算符可以省略。这类模板实参可以匹配指针类型的非类型参数
  • 对于引用类型的非类型模板参数,前面没有&运算符的外部变量和外部函数也是可取的。
  • 一个指向成员的常量指针:换句话说,类似&C::m的表达式,其中C是一个class类型,m是一个非静态成员(成员变量或者函数)。这类实参只能匹配为成员指针的非类型参数

当实参匹配指针类型或者引用类型的参数时,用户定义的类型转换(比如单参数的构造函数和重载类型转换运算符)和由派生类到基类的类型转换,都是不会被考虑的:即使在其他情况下,这些隐式类型转换都是有效的,但在这里是无效的。隐式类型转换的唯一应用只能是:给实参加上关键字const或者volatile

下面是一些有效的非类型模板实参的例子:

typename<typename T, T nontype_param>
class C;

C<int, 33> *c1; //整型

int a;
C<int*, &a> * c2; // 外部变量的地址


void f();
void f(int);
C<void(*)(int), f> *c3; //函数名称:这里重载解析会选择f(int),f前面的&隐式省略了

class X{
	publicint n;
		static bool b;
};

C<bool, X::b> *c4 // 静态类成员是可取的变量(和函数)名称
C<int X::*, &X::n> *c5; // 指向成员的指针常量

template<typename T>
void template_func();
C<void, &temlpate_func<double>> * c6; // 函数模板实例同时也是函数

模板实参的一个普遍约束是:在程序创建的时候,编译器或者链接器要能够确定实参的值。如果实参的值要等到程序运行是才能够确定(比如,局部变量的地址),就不符合“模板是在程序创建的时候进行实例化”了

另外,有些常值不能作为有效的非类型参数:

  • 空指针常量
  • 浮点型值
  • 字符串

有关字符串的一个问题是:两个完全等同的字符串可以存储在两个不同的地址中。在此,我们用一种很笨的解决方法来表达需要基于字符串进行实例化的模板:引入一个额外的变量来存储这个字符串

template<char const *str>
class Message;

extern char const hello[] = "Helloworld!";
Message<hello>* hello_msg;

可以看到,我们使用了关键字extern。因为如果不使用这个关键字,上面的const数组将具有内部链接

下面给出了一些错误的例子:

template<typename T, T nontype_param>
class C;

class Base{
	public:
		int i;
}base;


class Derived: public Base{
	
}derived_obj;

C<Base*, &derived_obj> *error1; //错误:这里不会考虑派生类到基类的类型转换
C<int*, base.i> *error2 ; // 错误:域运算符(.)后面的变量不会被看成是变量

int a[10];
C<int*, &a[0]> *error3; //错误:单一数组元素的地址并不是可取的

模板的模板实参

模板的模板实参必须是一个类模板,它本身具有参数,该参数必须精确匹配它所替换的模板的模板参数本身的参数。

#include <list>

template<typename T1, typename T2,
        template<typename > class Container>
class Relation{
    private:
        Container<T1> dom1;
        Container<T2> dom2;
};

int main(){
    Relation<int, double, std::list> rel; //错误:std::list是一个具有2个参数的模板
}

这里的问题是:标准库中的std::list模板具有两个参数,它的第2个参数(内存分配器allocator)具有一个缺省值,但是我们匹配std::list和Container参数时,并不会考虑这个值

这是,我们可以通过给模板的模板参数一个缺省值来解决这个问题:

template<typename T1, typename T2,
        template<typename T, typename = std::allocator<T>> class Container>
class Relation{
    private:
        Container<T1> dom1;
        Container<T2> dom2;
};

实参的相等性

当每个对应实参值都相等时,我们就成这两组模板实参是相同的:

template<typename T, int I>
class Mix;

typename int Int;

Mix<int, 3*3> *p1;
Mix<int, 9> *p2;  // p1和p2的类型是相同的

另外,从函数模板产生(即实例化出来)的函数一定不会等于普通函数,即便这两个函数具有相同的类型和名称。这样,针对类成员,我们可以引申出两点结论:

  • 从成员函数模板产生的函数永远也不会改写一个虚函数(进一步说明成员函数模板不能是一个虚函数)
  • 从构造函数模板产生的构造函数一定不会是缺省的拷贝构造函数(类似,从赋值运算符模板产生的赋值运算符也一定不会是拷贝赋值运算符。但是,后面的这种情况通常不会出现问题,因为和拷贝构造函数不同的是:复制运算符永远也不会被隐式调用)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值