-
2021-04-04 01:59:27
前言
前面我们介绍了函数模板的简单使用,但对于函数模板生成具体函数的时机我们并不知道。实际上,我们前面都采用了隐式实例化的方式来生成模板函数。
模板的实例化指函数模板(类模板)生成模板函数(模板类)的过程。对于函数模板而言,模板实例化之后,会生成一个真正的函数。而类模板经过实例化之后,只是完成了类的定义,模板类的成员函数需要到调用时才会被初始化。模板的实例化分为隐式实例化和显示实例化。
对函数模板的使用而言,分为两种调用方式,一种是显示模板实参调用(显示调用),一种是隐式模板实参调用(隐式调用)。对于类模板的使用而言,没有隐式模板实参和显式模板实参使用的说法,因为类模板的使用必须显示指明模板实参。各个概念请勿混淆。
首先定义函数模板:
template <class T> const<
更多相关内容 -
C/C++编程:模板实参
2021-04-19 20:27:31模板实参是指:在实例化模板时,用来替换模板参数的值。我们可以使用下面几种不同的机制来确定这些值: 显式模板实参:紧跟在模板名称后面,在一对<>内部的显式模板实参值。所组成的整个实体称为template-id ...模板实参是指:在实例化模板时,用来替换模板参数的值。我们可以使用下面几种不同的机制来确定这些值:
- 显式模板实参:紧跟在模板名称后面,在一对
<>
内部的显式模板实参值。所组成的整个实体称为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{ public: int 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的类型是相同的
另外,从函数模板产生(即实例化出来)的函数一定不会等于普通函数,即便这两个函数具有相同的类型和名称。这样,针对类成员,我们可以引申出两点结论:
- 从成员函数模板产生的函数永远也不会改写一个虚函数(进一步说明成员函数模板不能是一个虚函数)
- 从构造函数模板产生的构造函数一定不会是缺省的拷贝构造函数(类似,从赋值运算符模板产生的赋值运算符也一定不会是拷贝赋值运算符。但是,后面的这种情况通常不会出现问题,因为和拷贝构造函数不同的是:复制运算符永远也不会被隐式调用)
- 显式模板实参:紧跟在模板名称后面,在一对
-
C++模板学习笔记——模板实参
2021-10-15 11:19:02对于函数模板,编译器通过隐式推断模板实参。其中,从函数实参来确定模板实参的过程被称为模板实参推断。在模板实参推断过程中,编译器使用函数调用中的实参类型来寻找模板实参,用这些模板实参生成的函数版本与给定...对于函数模板,编译器通过隐式推断模板实参。其中,从函数实参来确定模板实参的过程被称为模板实参推断。在模板实参推断过程中,编译器使用函数调用中的实参类型来寻找模板实参,用这些模板实参生成的函数版本与给定的函数调用最为匹配;
类型转换与模板类型参数:
对于模板函数和普通函数而言,在函数调用过程中实参将会用来初始化函数的形参;
在模板函数中,采用特殊的初始化规则(编译器通常不会对实参进行类型转换,而是生成一个新的模板实参)。
- const转换:
可以将一个非const对象的引用(或指针)传递给一个const的引用(或指针)形参 - 数组或函数指针转换:
如果函数形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转化。
其他类型转换(算术转换、派生类向基类的转换、用户自定义转换)都不能应用于函数模板。
template<typename T> T fobj(T,T); template<typename T> T fref(const T&,const T&); string s1("a value"); const string const_s2("another value"); fobj(s1,const_s2); //调用fobj(string,string);const被忽略 fref(s1,const_s2); //调用fref(const string&,const string&) int a[10],b[43]; fobj(a,b); //调用f(int*,int*) fref(a,b); //错误:数组类型不匹配(函数形参是引用类型)
将实参传递给带模板类型的函数型参时,能够自动应用的类型转换只有const转换及数组或函数到指针的转换。
- 使用相同模板参数类型的函数形参:由于模板函数只允许有限的几种类型转换,因此传递给这些形参的实参必须具有相同的类型。
- 正常类型转换应用于普通函数实参:在模板函数形参中,未涉及模板类型参数的类型不进行特殊处理,他们正常转换为对应形参的类型;
template<typename T> ostream &print(ostream& os,const T& obj){ return os >> obj; } print(cout,32); //实例化print(ostream&,int); ofstream f("output"); print(f,10); //实例化print(ostream&,int); 将f转换为了ostream&
如果函数参数类型不是模板参数,则对实参进行正常的类型转换
函数模板显式实参
在某些情况下编译器无法判断出模板实参的类型。其他情况下,我们允许用户控制模板实例化。
- 指定显式模板实参
作为一个允许用户指定使用类型的例子,我们将定义一个名为sum的函数模板。它接受两个不同类型的参数。我们希望允许用户指定结果的类型,这样,用户就可以选择合适的精度。
template<typename T1,typename T2,typename T3> T1 sum(T2,T3); //T1是显式指定的,T2和T3式从函数实参类型推断得来的 auto val3 = sum<long long>(i,lng); //long long sum(int,long);
显式模板实参按从左至右的顺序与对应的模板参数匹配; 看下面的代码:
template<typename T1,typename T2,typename T3> T3 sum (T1,T2); //对于此种情况,我们必须为所有三个形参指定实参 auto val3 = sum<long long,int,long>(i,lng);
- 正常类型转换应用于显式指定的实参:
对于模板类型参数已经指定了的函数实参,可以进行正常的类型转换
尾置返回类型于类型转换
但我们希望用户确定返回类型时,用显式模板实参表示模板函数的返回类型是很有效大的。 但在其他情况下,要求显示指定模板实参会给用户增添额外负担,而且不会带来好处。
例如,我们希望编写一个函数,接受表示序列的一对迭代器和返回类型中一个元素的引用:
template<typename It> auto fcn(It beg,It end) -> decltype(*beg){ //decltype可以获取变量的类型 //处理序列 return *beg; //返回序列中一个元素的引用 } std::string s1("abcdefg"); auto ret = fcn(s1.begin(),s1.end()); //此时ret的返回类型为char&
- 进行类型转换的标准库模板类
有时我们无法直接获取所需要的类型。例如,我们可以希望编写一个类型fcn的函数,但返回一个元素的值而非引用。
template<typename It> auto fcn2(It beg,It end) -> typename remove_reference<decltype(*begin)>::type{ //处理序列 return *beg; //返回序列中一个元素的拷贝 }
函数指针与实参推断
当我们用一个函数模板初始化一个函数指针或为一个函数指针赋值时,编译器使用指针的类型来推断模板实参
例如,假定我们有一个函数指针,它指向的函数返回int,接受两个参数,每个参数都是指向const int的引用。我们可以使用该指针指向compare的一个实例
template<typename T> int compare(const T&,const T&); //pf1指向实例int compare(const int&,const int&); int (*pf1)(const int&,const int&) = compare;
当参数是一个函数模板实例的地址时,程序上下文必须满足:对每个模板 参数,能唯一确定其类型或值
模板实参推断和引用
template <typename T> void f(T &p);
函数参数p是一个模板类型参数T的引用,编译器会应用正常的引用绑定规则:const是底层的,不是顶层的
- 从左值引用函数参数推断类型
当一个函数参数是模板类型参数的一个普通(左值)引用时(T&),绑定规则告诉我们,只能传递给它一个左值。实参可以是const类型,也可以不是。如果实参是const的,则T将被推断为const类型(const T&):
template<typename T> void fl(T&); fl(i); // i 是一个int;模板参数类型T是int fl(ci); //ci是一个const int;模板参数T是const int fl(5); // 错误,传递给一个&参数的实参必须是一个左值
如果一个函数参数的类型是const T&,正常的绑定规则告诉我们可以传递给它任何类型的实参—— 一个对象(const or not const )、一个临时对象或是一个字面常量值。
template<typename T> void f2(const T&); //f2 中的参数是const& ;实参中的const是无关的 //在每个调用中,f2的函数实参都被推断为const T& f2(i); // i 是一个int,模板参数T是int; f2(ci); //ci 是一个const int, 但模板参数T 是int f2(5); //一个const& 参数可以绑定到一个右值;T是int
- 从右值引用函数参数推断类型
template<typename T> void f3(T&&);
当一个函数参数是一个右值引用(T&&)时,正常绑定规则告诉我们可以传递给他一个右值。
-
引用折叠和右值引用参数
通常我们不能将一个右值引用绑定到一个左值上。但是,c++语言在正常绑定规则之外定义了两个例外规则,允许这种绑定
- 第一个例外规则影响右值引用参数的推断如何进行。当我们将一个左值传递给函数的右值引用参数,且此右值引用指向模板类型参数(如T&&),编译器推断模板类型参数为实参的左值引用类型。 例如,当调用f3(i)时,编译器推断T的类型为int&,而非int。
可以将上述情况理解为定义了一个了类型为int& 的右值引用 ,通常情况下,我们不能(直接)定义一个引用的引用,但是,通过类型别名或通过模板类型参数间接定义是可以的。 - 第二个例外绑定规则:如果我们间接创建一个引用的引用,则这些引用形成了"折叠"。在所有情况下(除了一个例外),引用会折叠成一个普通的左值引用类型。在新标准中,折叠规则扩展到了右值引用。只有在一种情况下引用会折叠成右值引用:右值引用的右值引用。X& &、X& && 和 X&& &都折叠为类型 X& ; 类型X&& && 折叠为X&&
引用折叠只能应用于间接创建的引用的引用,如类型别名或模板参数
void f3<int&>( int&); //当T是int&时,函数折叠为int&
如果一个函数参数是指向模板类型参数的右值引用,则可以传递给它任意类型的实参。如果将一个左值传递给这样的参数,则函数参数被实例化为一个普通的左值引用。
- 编写接受右值引用参数的模板函数
template<typename T> void f3(T&& val){ T t = val; //拷贝还是绑定一个引用? t = fcn(t); //赋值只改变t还是既改变t又改变val? if(val == t){...} //若T是引用类型,则一直为true }
在实际情况中,右值引用通常用于两种情况:模板转发其实参 或 模板被重载。
-
转发
某些函数需要将其一个或多个实参连同类型不变地转发给其他函数。再此情况下,我们需要保持被转发实参的所有性质,包括实参是否是const的以及实参是左值还是右值。
//接受一个可调用对象和另外两个参数的模板 //对“翻转”的参数调用给定的可调用对象 //flip1是一个不完整的实现:顶层const和引用丢失 template <typename F,typename T1,typename T2> void flip1(F f,T1&& t1,T2&& t2){ f(t2,t1); }
如果一个函数参数是指向模板类型参数的右值引用,他对应的实参的const属性和左值/右值属性将得到保持;
-
在调用中使用std::forward保持类型信息
forward返回该显式实参类型的右值引用。即,forward<T>的返回类型为T&&
template<typename F,typename T1,typename T2> void flip(F f,T1 &&t1,T2 &&t2){ f(std::forward<T2>(t2),std::forward<T1>(t1)); }
当用于一个指向模板参数类型的右值引用函数参数时,forward会保持实参类型的所有细节
参考文献:C++ Primer(第五版)
本文仅为本人学习笔记,仅做记录用途 - const转换:
-
模板的显示>隐式>体化、显示>隐式>调用和【模板特化】理解
2020-07-28 20:00:37函数模板的调用方式3.1 隐式模板实际参数调用3.2 显示模板实参调用4. 模板特化4.1 模板特化的定义4.2 函数模板特化4.2.1 使用函数重载替代函数模板特化4.3 类模板特化5. 总结 C++模板之隐式体化、显示体化、隐式...目录
C++模板之隐式体化、显示体化、隐式调用、显示调用和模板特化区分
- 函数模板:中心词是模板,所以是指未体化的针对函数的模板
- 模板函数:中心词是函数,所以是指模板体化后的实际可使用的函数
- 类模板:中心词是模板
- 模板类:中心词是类
模板的体化指函数模板(类模板)生成模板函数(模板类)的过程。对于函数模板而言,模板体化之后,会生成一个真正的函数。而类模板经过体化之后,只是完成了类的定义,模板类的成员函数需要到调用时才会被初始化。模板的体化分为隐式体化和显示体化。
- 对于函数模板的使用而言,分为两种调用方式,一种是显示模板实参调用(显示调用),一种是隐式模板实参调用(隐式调用)。
- 对于类模板的使用而言,没有隐式模板实参和显式模板实参使用的说法,因为类模板的使用必须显示指明模板实参。各个概念请勿混淆。
1. 隐式体化
1.1 模板隐式体化的定义
这是相对于模板显示体化而言。在使用模板函数和模板类时,不存在指定类型的模板函数和模板类的实体时,由编译器根据指定类型参数隐式生成模板函数或者模板类的实体称之为模板的隐式体化。
1.2 函数模板隐式体化
函数模板隐式体化指的是在发生函数调用的时候,如果没有发现相匹配的函数存在,编译器就会寻找同名函数模板,如果可以成功进行参数类型推演,就对函数模板进行体化。
还有一种简介调用函数的情况,也可以完成函数模板的体化。所谓的简介调用是指将函数入口地址传给一个函数指针,通过函数指针完成函数调用。如果传递给函数指针不是一个真正的函数,那么编译器就会寻找同名的函数模板进行参数推演,进而完成函数模板的体化。参考如下示例。
#include <iostream> using namespace std; template <typename T> void func(T t){ cout<<t<<endl; } void invoke(void (*p)(int)){ int num=10; p(num); } int main(){ invoke(func); } // 程序成功运行并输出10。
1.3 类模板隐式体化
类模板隐式体化指的是在使用模板类时才将模板体化,相对于类模板显示体化而言的。考察如下程序。
#include <iostream> using namespace std; template<typename T>class A{ T num; public: A(){ num=T(6.6); } void print(){ cout<<"A'num:"<<num<<endl; } }; int main(){ A<int> a; //显示模板实参的隐式实例化 a.print(); } // 程序输出结果:A’num:6
2. 显示体化
2.1 模板显示体化的定义
显示体化也称为外部体化。在不发生函数调用的时候讲函数模板体化,或者在不使用类模板的时候将类模板体化称之为模板显示体化。
2.2 函数模板的显示体化
对于函数模板而言,不管是否发生函数调用,都可以通过显示体化声明将函数模板实体化,格式为:
template [函数返回类型] [函数模板名]<实际类型列表>(函数参数列表)
例如:
template void func<int>(const int&); // 显示体化声明前面必须加上关键字 template
2.3 类模板的显示体化
对于类模板而言,不管是否生成一个模板类的对象,都可以直接通过显示体化声明将类模板实体化,格式为:
template class theclass<int>; // 声明时,必须加上关键字template
3. 函数模板的调用方式
3.1 隐式模板实际参数调用
在发生函数模板的调用时,不显示给出模板参数而经过参数推演,称之为函数模板的隐式模板实参调用(隐式调用)。
template <typename T> void func(T t){ cout<<t<<endl; } func(5); //隐式模板实参调用
3.2 显示模板实参调用
在发生函数模板的调用时,显示给出模板参数而不需要经过参数推演,称之为函数模板的显示模板实参调用(显示调用)。
- 显示模板实参调用在参数推演不成功的情况下是有必要的。考察如下程序。
#include <iostream> using namespace std; template <typename T> T Max(const T& t1,const T& t2){ return (t1>t2)?t1:t2; } int main(){ int i=5; //cout<<Max(i,'a')<<endl; //无法通过编译 cout << Max<int>(i,'a') << endl; //显示调用,通过编译 }
直接采用函数调用
Max(i,’a’)
会产生编译错误,因为i
和’a’
具有不同的数据类型,无法从这两个参数中进行类型推演。而采用Max< int>(i,’a’)
调用后,函数模板的体化不需要经过参数推演,而函数的第二个实参也可以由char转换为int型,从而成功完成函数调用。编程过程中,建议采用显示模板实参的方式调用函数模板,这样提高了代码的可读性,便于代码的理解和维护。
4. 模板特化
4.1 模板特化的定义
模板特化不同于模板的体化,模板参数在某种特定类型下的具体实现称为模板的特化。模板特化有时也称之为模板的具体化,分别有函数模板特化和类模板特化。
4.2 函数模板特化
函数模板特化是在一个统一的函数模板不能在所有类型实例下正常工作时,需要定义类型参数在体化为特定类型时函数模板的特定实现版本。简单理解就是:针对特定类型参数的定制版。查看如下例子。
#include <iostream> using namespace std; template<typename T> T Max(T t1,T t2){ return (t1>t2)?t1:t2; } typedef const char* CCP; template<> CCP Max<CCP>(CCP s1,CCP s2){ return (strcmp(s1,s2)>0)?s1:s2; } int main(){ //调用实例:int Max<int>(int,int) int i=Max(10,5); //调用显示特化:const char* Max<const char*>(const char*,const char*) const char* p=Max<const char*>("very","good"); cout<<"i:"<<i<<endl; cout<<"p:"<<p<<endl; } // 程序正常编译运行结果: i:10 p:very
在函数模板显示特化定义(Explicit Specialization Definition)中,显示关键字
template
和一对尖括号<>
,然后是函数模板特化的定义。该定义指出了模板名、被用来特化模板的模板实参,以及函数参数表和函数体。在上面的程序中,如果不给出函数模板Max<T>
在T
为const char*
时的特化版本,那么在比较两个字符串的大小时,比较的是字符串的起始地址的大小,而不是字符串的内容在字典序中先后次序。4.2.1 使用函数重载替代函数模板特化
除了定义函数模板特化版本外,还可以直接给出模板函数在特定类型下的重载形式(普通函数)。使用函数重载可以实现函数模板特化的功能,也可以避免函数模板的特定实例的失效。例如,把上面的模板特化可以改成如下重载函数。
typedef const char* CCP; CCP Max(CCP s1,CCP s2){ return (strcmp(s1,s2)>0)?s1:s2; }
程序运行结果和使用函数模板特化相同。但是,使用普通函数重载和使用模板特化还是有不同之处,主要表现在如下两个方面:
(1)如果使用普通重载函数,那么不管是否发生实际的函数调用,都会在目标文件中生成该函数的二进制代码。而如果使用模板的特化版本,除非发生函数调用,否则不会在目标文件中包含特化模板函数的二进制代码。这符合函数模板的“惰性体化”准则。
(2)如果使用普通重载函数,那么在分离编译模式下,应该在各个源文件中包含重载函数的申明,否则在某些源文件中就会使用模板函数,而不是重载函数。4.3 类模板特化
类模板特化类似于函数模板的特化,即类模板参数在某种特定类型下的定制化实现。考察如下代码。
#include <iostream> using namespace std; template<typename T>class A{ T num; public: A(){ num=T(6.6); } void print(){ cout<<"A'num:"<<num<<endl; } }; template<>class A<char*>{ char* str; public: A(){ str="A' special definition "; } void print(){ cout<<str<<endl; } }; int main(){ A<int> a1; //显示模板实参的隐式实例化 a1.print(); A<char*> a2; //使用特化的类模板 A2.print(); } // 程序输出结果如下: A’num:6 A’ special definition
5. 总结
隐式体化指的是:在使用模板之前,编译器不生成模板的声明和定义实体。只有当使用模板时,编译器才根据模板定义生成相应类型的实体。如:
int i=0, j=1;swap(i, j);
//编译器根据参数i,j的类型隐式地生成swap<int>(int &a, int &b)
的函数定义。Array<int> arVal;
//编译器根据类型参数隐式地生成Array<int>
类声明和类函数定义。显式体化:当显式实体化模板时,在使用模板之前,编译器根据显式体化指定的类型生成模板实体。如前面显示实体化(explicit instantiation)模板函数和模板类。其格式为:
template returntype functionname<typename>(argulist); template class classname<typename>;
显式实体化只需声明,不需要重新定义。编译器根据模板实现实体声明和实体定义。特化:对于某些特殊类型,可能不适合模板实现,需要重新定义实现,此时可以使用特化(explicite specialization)。特化需重新定义。格式为:
template<> returnname function<typename>(argu_list){...};
template<> class classname<typename>{...};
-
C++模板之隐式实例化、显示实例化、隐式调用、显示调用和模板特化详解
2018-04-17 21:09:39代码编译运行环境:VS2012+Debug+Win32 模板的实例化指函数模板(类模板)生成模板函数(模板类)的过程。对于函数模板而言,模板实例化之后,...模板的实例化分为隐式实例化和显示实例化。 对函数模板的使... -
C++:50---模板实参推断(附加:模板显式实参、type_traits、引用折叠、move()、forward())
2020-02-24 21:19:47在模板实参推断过程中,编译器使用函数调用中的实参类型来寻找模板实参,用这些模板实参生成的函数版本与给定的函数调用最为匹配 二、类型转换与模板类型参数 类型转换 与非模板函数一样,我们再一次调用中传递... -
C++---显示实例化与隐式实例化,显示调用与隐式调用
2020-01-01 12:22:10出现场景:C++模板中 template<class T> T Add(T left,T right) return left+right; 上述代码只有经过实例化之后才会形成真正...函数模板实例化之后会生成一个真正的函数,但是由于类中的成员函数只有被调用... -
在哪些情况需要显示指定函数模板实参?
2017-09-22 09:17:12也就是,调用参数类型隐含着模板参数类型,如果能够推导出模板参数,那么就不需要显示指定。例如:template const T& Max(const T& a,const T& b) { return a>b?a:b; } Max(3,5); 函数实参3和5都是int类型,我们... -
Java中父类构造器的隐式调用和显式调用
2021-02-12 20:55:04当创建任何 Java 对象时,程序总会先依次调用每个父类非静态初始化块、父类构造器(总是从 Object 开始,因为Java 程序里所有类的最终父类都是 java....这个调用是隐式执行的,而且父类的静态初始化块总是会被执行... -
002 模板实参推断、重载与模板
2018-07-27 09:52:00一、模板函数显示实参 情况1: template <typename T1, typename T2, typename T3> T1 sum(T2 a, T3 b) { return a + b; } 分析:调用的时候就需要指定T1的类型,如:sum<float>(1, 2); ... -
c++中指定显式模板实参问题
2015-12-17 09:39:02在C++中,若函数模板返回类型需要用户指定,那么在定义函数模板时,模板参数的顺序是很重要的,如下代码: template T1 sum(T2 a, T3 b){ return a + b; } 在调用的时候就需要指定T1的类型,如:sum(1,2);于是... -
【第十六章】模板实参推断
2019-05-08 20:14:52二、模板显式推断 在C++中,若函数模板返回类型需要用户指定,那么在定义函数模板时,模板参数的顺序是很重要的,如下代码: template <...在调用的时候就需要指定T1的类型,如:sum<float&... -
Java 方法的使用(方法重载、形参和实参调用关系、方法递归)
2021-05-01 13:09:18Java 方法的使用(方法重载、形参和实参调用关系、方法递归)前言一、方法重载二、实参和形参的传递三、方法递归 前言 提示: 本文介绍了方法的三种实际的使用 方法重载、实参与形参传递的形式(有画内存图)方法... -
[C++ Template]深入模板--模板实参演绎
2018-11-25 21:24:12目录 ...11.6 缺省调用实参 第11章 模板实参演绎 在每个函数模板的调用中, 如果都显式地指定模板实参(例如,concat<std::string, int>(s,3) ) , 那么很快就会导致很繁琐的... -
C/C++编程:模板实参演绎的过程
2021-04-20 14:35:31为什么需要模板实参演绎 在某个函数模板的调用中,如果都显式的指定模板实参(比如concat<...针对一个函数调用,演绎过程会比较调用实参的类型和函数模板对应的参数化类型(即T),然后针对要被演绎 ... -
C++Primer_Chap16_模板和泛型编程_List02_模板实参推断_笔记
2018-11-04 15:11:29从函数实参类确定模板实参的过程称为模板实参推断(template argument deduction)。 类型转换和模板类型参数 如果一个函数形参的类型使用了模板类型参数,那么它采用特殊的初始化规则。只有很有限的几种类型... -
【现代C++】第三部分 类设计工具(4) 函数/类/成员模板、模板实参推断、重载与模板、可变参数模板、模板特例...
2021-10-12 09:08:45当使用模板时,我们(隐式或显式地)指定模板实参 template argument 并将其绑定到模板参数上。 上面的 compare 函数声明了一个名为 T 的类型参数,因此在 compare 中,可以用名字 T 表示一个类型。而 T 表示的实际... -
C++:模板实参推断及引用折叠
2015-06-14 11:00:211.模板实参推断的概念对于函数模板和类模板,编译器利用调用中的实参函数来确定其模板参数。 从函数实参来确定模板实参的过程,称为模板实参推断。 模板实参的推断和常规的函数实参推断的规则不一样,尤其是在类型... -
C++模板与泛型编程:模板实参推断与引用,理解std::move,与转发 (std::forward)
2020-03-12 19:41:49文章目录模板实参推断与引用从左值引用函数参数推断类型从右值引用函数参数推断类型引用折叠和右值引用参数编写接受右值引用参数的模板函数理解 std::movestd::move 是如何定义的std::move 是如何工作的从左值 ... -
关于C++数组的引用,以及模板函数中的隐式类型转换规则
2020-05-31 11:31:43数组或函数指针转换:如果函数形参不是引用类型(注意不是引用类型的才可以),则可以对数组或函数类型的实参应用正常的指针转换(也就是数组或函数传给形参的时候是隐式转换成指针的),一个数组实参可以转换成一个... -
【C++ Primer 第16章】2. 模板实参推断
2018-05-24 13:57:00模板实参推断:对于函数模板,编译器利用调用中的函数实参来确定模板参数,从函数实参来确定模板参数的过程被称为模板实参推断。 类型转换与模板类型参数 与往常一样,顶层const无论在形参中还是在是实参中,都被... -
函数模板与重载时的调用规则
2019-01-29 15:11:28对于这种 1.函数的业务逻辑一样 2.函数的参数类型不一样 的多次重复型函数,为了节省开发工作,提出泛型编程 泛型编程:即用template定义一个泛型T,用...1.1函数模板分为显示调用和隐式调用,即主动地声明模板函数... -
【C++知识】模板与泛型编程
2019-08-14 21:19:21我们只编写一次模板,就可以将其用于多种类型和值,编译器会为每种类型和值进行模板实例化。这一章内容有点儿多,需要大家慢慢看和理解,需要了解更多详细知识,建议自行查看书籍,这里主要介绍一些细节。 最后,... -
C++ 模板实例化与调用
2015-10-29 13:13:41模板的实例化指函数模板(类模板)生成模板函数(模板类)的过程。对于函数模板而言,模板实例化之后,会生成一个真正的函数。而类模板经过实例化之后,只是完成了类...模板的实例化分为隐式实例化和显示实例化。...... -
模板隐式和显式实例化
2018-03-04 20:54:34C++中模板的实例化指函数模板(类模板)生成模板函数...1.隐式实例化1.1模板隐式实例化的定义这是相对于模板显示实例化而言。在使用模板函数和模板类时,不存在指定类型的模板函数和模板类的实体时,由编译器根...