所谓templates arguments是当编译器实例化一个template时用来替换template parameter的值。编译器以数种不同的机制来决定以何值替换template parameters:
explicit template arguments(显式指定模板自变量):可在template名称之后跟着一个或多个明确的template arguments,并以角括号括起来。这个完整的名称称为template-id。
injected class name (内植式类别名称):在带有参数P1,P2....的class template X作用域中,
template X的名称与template-id X<P1, P2, ...>等价。
default template arguments: 如果存在可用的default template arguments.我们可在class template具体实现中省略不写 explicit template arguments.然而,即使每一个参数都有默认值,你也必须把开闭两个角括号写上(即使括号内什么也没有)。
argument deduction(自变量推导):编译器可根据function call arguments推导出function template argument。其他某些情况下,编译器也可能动用推导机制。如果所有template argument都可以推导得出,你就无需在function template名称后面加写角括号。
1. 函数式模板自变量Function Template Argument
既可以指定function template 的 template argument,也可以让它们被编译器推导出来。例如,
template <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) ; // explict template arguments
max(1.0,-3.0); // template argument, 被编译器隐式推导为double
max<int> (1.0, -3.0); //明确指定为int以抑制推导,从而实参为int类型
}
有些模板实参无法被推导所得,最好把这类参数放在template parameter list的最前面,这样客户端或者说调用者只需明白指定编译器无法推导的那些实参即可,其余实参仍然可以被自动推导得到。例如,
template <typename DstT, typename SrcT>
inline DstT implicit_cast (SrcT const& x) //SrcT 可被推导但DstT无法推导。
//因为DstT没有出现在参数列表中,无法推导。
{
return x;
}
int main()
{
double value= implicit_cast<double>(-1);
}
如果不这么写,需要把上面的template parameters的顺序换一下(写成template<typename SrcT, typename DstT>),我们就不得不在调用implicit_cast时把两个template argument都明确写出来。
由于function template 可被重载overload,因此即使明确写出一个function template的所有自变量,可能也不足以使编译器确定该调用哪一份具体函数,因为某些情况下符合要求的函数可能有一群。下面的例子,说明这种情况:
template <typename Func, typename T>
void apply(Func func_prt, 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()
{
apply(&single<int>, 3); //OK
apply(&multi<int>, 7);
}
在这个例子中,对apply()的第一个调用动作是合法的,因为表达式&single<int>没有任何歧义,因此template parameter Func的值可被编译器轻易推导出来。然而在第二个调用动作中,&multi<int>可以是两个不同类型中的任何一个,这种情况下编译器无法推导出Func。
不仅如此,明确指定template argument还可能导致构建出不合法的C++类型,考虑下面的重载函数,其中RT1和RT2是未定类型:
template <typename T> RT1 test(typename T::X const*);
template <typename T> RT2 test(...);
算式test<int>对于第一个function template而言是无意义的,因为int类型并没有member type X。然而第二个function template 无此问题。因此算式&test<int>可确定出唯一一个函数地址。
SFINAE,Substitution-Failure-Is-Not-A-Error,替换失败并非错误,这一原则,使得function
template的重载实际可行,而且借助这个原则,创造出很多极其出色的编译期技术compile-time techniques。例如,假设类型RT1和RT2分别定义为:
tyepdef char RT1;
typedef struct {char a[2]; } RT2;
我们可以在编译期判断某个给定类型T是否有member type x:
#define type_has_member_type_X(T) \
(sizeof(test<T>(0)) == 1)
我们可以从外向内分析上面的宏。
首先,如果第一个test template被编译器选中,sizeof算式值等于1(返回类型为char, 大小为1)。如果第二个template被选用,则sizeof算式值至少为2(返回类型是内含两个char的array)。换句话说,这个宏的作用是判别选用了哪一个function template。
很明显,如果给定的类型T不存在member Type X,第一个function template不会被选用。然而,如果给定的类型T存在member type X,根据重载解析规则,第一个function template会被优先选用;这是因为重载解析规则会优先考虑“把0值转换为一个null指针”,而不考虑“把0值当作省略号”
(省略号参数是重载解析规则中最后才考虑是否匹配的参数形式)。
上述的SFINAE原则只是协助你避免创造出非法类型,并不能防止非法算式(invalid expression)被编译器求值(evaluated)。因此下面例子是不合法的:
template<int I> void f(int (&) [24 / (4 - I)]);
int main()
{
&f<4>; //Error, 分母为0
}
下面例子是合法的:
template<int N> int g() {return N; }
template <int* P> int g() {return *P; }
int main()
{
return g<1>(); //1 无法适用于int* 参数,然而SFINAE原则在这里起来作用。
}
2.类型实参Type Argument
template type arguments是针对template type parameters而指定的值。我们惯用的大多数types都可以作为template arguments使用,但有两个例外:
1)local classes和local enum types不能作为template type arguments使用
2)如果某个type涉及无名的unamed class type或无名的unamed enum types,这样的type不能作为template type arguments使用,但如果运用typedef 使其具名便可用做template type arguments
下面例子展示上面说的两种例外情况:
template <typenae T> class List{
...
};
typedef struct{
double x,y,z;
} Point;
typedef enum {red, green,blue} *ColorPtr;
int main()
{
struct Association
{
int* p;
int* q;
};
List<Assocaition*> error1; //Error:模板实参不能是Local Type
List<ColorPtr> error; //Error: 模板实参不能是没有名字的类型unamed type
List<Point> ok; //OK, Point有了名称
}
另外,需要注意的是,类型参数替换后,其结果必须是个合法的C++构件constructs:
template <typename T>
void clear (T p)
{
*p =0;
}
int main()
{
int a;
clear(a);
}
3.非类型实参 Nontype Argument
Nontype template arguments是针对nontype template parameters而指定的值,它必须符合下列条件之一:
1)是一个具有正确类型的nontype template parameter
2)是一个编译期整数型或列举型常数,但必须与对应的参数类型匹配,或者可隐式转换为该类型
3)以内建一元取址运算符(unary address of operator&)为前导的外部变量或函数。面对函数或array变量,可省略不写‘&’。这一类template arguments匹配的是pointer nontype parameter。
4)如上所述,无前导‘&’,匹配的是reference type nontype parameter
5)一个pointer-to-member常数,形如 &C::m的表达式其中C是class type。m是non-static成员)(函数或变量)。它只匹配pointer-to-membertype nontype parameters
下面例子中的nontype template arguments都合法:
template <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; //一个函数名称
class X{
public:
int n;
static bool b;
}
C<boo&, &X::b>* c4; //static class members都可接受
C<int X::*, &X::n> c5; // pointer to member 常数
template <typename T>
void templ_func();
C<void(), &templ_func<double> >* c6; //function template 实例化后也是一种函数
另外,不能使用如下各种常数值作为模板实参:
- null pointer常数
- 浮点数float point number
- 字符串字面常量string literals
两个内容完全相同的字符串常数可能存在两个不同的地址上。
以下还有几个非法例子:
template <typename T, T nontype_param>
class C;
class Base{
public:
int i;
} base;
class Derived: public Base {
} derived_obj;
C<Base*, &derived_obj>* err1; //Error: derived to Base不被考虑
C<int&, bas.i>* err2; //成员变量不被考虑
int a[10];
C<int*, &a[10]>* err3; //Error:不能使用array内某个元素的地址
4.双重模板实参Template Template Argument
template template argument必须是这样一个class template:其参数完全匹配待替换之template template parameter的参数。template template argument的default argument会被编译器忽略,除非对应的template template parameter有预设自变量。
#include <list>
template <typename
T1,
typename
T2,
template <typename > class Container> //Container只带一个参数
class Relation{
public:
...
private:
Container<T1> dom1;
Container<T2> dom2;
};
int main()
{
Relation<int, double, std::list> rel; //Error: std::list有不止一个参数
...
}
问题出在标准库的 std::list template拥有不止一个参数。第二个参数(是个allocator,配置器)有默认值,但编译器把 std::list 匹配至Container时,该默认值被忽略了。
我们需要为template template parameter加上一个带有默认值的参数即可。
#include <memory>
template <typename T1,
typename
T2,
template <typename T,
typename = std::allocator<T> > class Container>
class Relation{
public:
....
private:
Container<T1> dom1;
Container<T2> dom2;
};
5.等价Equivalence
当两组template arguments的元素一一对等时,我们称这两组自变量等价。对于type arguments, typedef的名称并不影响对比过程最终被比较的是typedef所指代的type。对于整型的nontype arguments,比较的是自变量值,与自变量表达式无关。下面例子解释刚才这个概念:
template <typename T, int I >
class Mix;
typedef int Int;
Mix<int , 3*3> * p1;
Mix<Int, 4+5>* p2; //p2和p1是同一类型,两者等价
一个由function template产生的函数,和一个常规函数,无论如何不会被编译器看作等价,即使它们的类型和名称完全相同。这对class members造成两个重要结果:
1)由member function template产生的函数不会覆盖虚函数virtual。
2)由construct template产生的构造函数不会被当作default copy构造函数(同样道理,由assignment template产生的assignment运算符不会被当作一个copy-assignment运算符)