[C++ Template]深入模板--模板实参演绎

目录

第11章 模板实参演绎

11.1 演绎的过程

11.2 演绎的上下文

11.3 特殊的演绎情况(比较晦涩,不理解)

11.4 可接受的实参转型

11.5 类模板参数

11.6 缺省调用实参


第11章 模板实参演绎

在每个函数模板的调用中, 如果都显式地指定模板实参(例如,concat<std::string, int>(s,3) ) , 那么很快就会导致很繁琐的代码。 幸运的是, 借助于功能强大的模板实参演绎过程, C++编译器通常都可以自动地确定这些所需要的模板实参(即隐式实例化)。

 

11.1 演绎的过程

针对一个函数调用, 演绎过程会比较“调用实参的类型”和“函数模板对应的参数化类型(即T) ”, 然后针对要被演绎的一个或多个参数,分别推导出正确的替换。 我们应该记住: 每个实参-参数对的分析都是独立的; 因此, 如果最后所得出的结论发生矛盾, 那么演绎过程将失败。考虑下面的例子:

template<typename T>
T const& max(T const& a, T const& b)
{
	return a < b ? b : a;
}
int g = max(1, 1.0);

即使所有被演绎的模板参数都可以一致性地确定(即不发生矛盾) , 演绎过程也可能会失败。 这种情况就是: 在函数声明中, 进行替换的模板实参可能会导致无效的构造。 请看下面的例子:

template<typename T>
typename T::ElementT at(T const& a, int i)
{
	return a[i];
} 
void f(int* p)
{
	int x = at(p, 7);
}

在此, T被演绎成int*(只有一个参数类型与T有关, 当然也就不会发生前面的分析矛盾) 。 然而, 在返回类型T::ElementT中, 用int*来替换T之后, 显然会导致一个无效的C++构造, 从而也使这个演绎过程失败。

我们接下来需要描述实参-参数对是如何进行匹配的。 我们会使用下面的概念来进行描述: 匹配类型A(来自实参的类型) 和参数化类型P(来自参数的声明) 。 如果被声明的参数是一个引用声明(即T&) ,那么P就是所引用的类型(即T) , 而A仍然是实参的类型。 否则的话,P就是所声明的参数类型, 而A则是实参的类型; 如果这个实参的类型是数组或者函数类型, 那么还会发生decay转型, 转化为对应的指针类型, 同时还会忽略高层次的const和volatile限定符。 例如:

template<typename T> void f(T); //P就是T
template<typename T> void g(T&); //P仍然是T
double x[20];
int const seven = 7;
f(x); //非引用参数(针对f): T是double*
g(x); //引用参数(针对g): T是double[20]
f(seven); //非引用参数: T是int.
g(seven); //引用参数: T是int const
f(7); //非引用参数: T是int
g(7); //引用参数: T是int =>错误: 不能把7传递给int&

我们已经知道: 对于引用参数, 绑定到该参数的实参是不会进行decay的。 然而, 如果我们遇到字符串类型的实参, 却总是会产生出人意料的结果。 重新考虑下面的模板:

template<typename T>
T const& max(T const& a, T const& b);

对于表达式max(“Apple”,“Pear”), 我们可能会期望T被演绎成charconst* 。 然而, “Apple”的类型是char const[6], 而“Pear”的类型是char const[5]; 而且不存在数组到指针的decay转型(因为要演绎的参数是引用参数) 。 因此, 为了使演绎成功, T就必须同时是char[6]和char[5];而这显然是不可能的, 因此这会产生错误。

 

11.2 演绎的上下文

对于比T复杂很多的参数化类型, 也可以与给定的实参进行匹配。下面是一些比较基础的例子:

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

template<typename E, int N>
void f2(E(&)[N]); //< 数组,引用参数

template<typename T1, typename T2, typename T3>
void f3(T1(T2::*)(T3*)); //< 类的成员函数指针

class S 
{
public:
	void f(double*);
}; 

void g(int*** ppp)
{
	bool b[42];
	f1(ppp); //演绎T为int**.
	f2(b); //演绎E为bool, N为42.
	f3(&S::f); //演绎T1=void,T2=S,T3=double.
}

复杂的类型声明都是产生自(比它) 基本的构造(例如指针、 引用、 数组、 函数声明子(declarators) ; 成员指针声明子、 template-id等) ; 匹配过程是从最顶层的构造开始, 然后不断递归各种组成元素(即子构造) 。 我们可以认为: 大多数的类型声明构造都可以使用这种方式进行匹配, 这些构造也被称为演绎的上下文。 然而, 某些构造就不能作为演绎的上下文, 例如:

受限的类型名称。 例如, 一个诸如Q<T>::X的类型名称不能被用来演绎模板参数T。

除了非类型参数之外, 模板参数还包含其他成分的非类型表达式。例如, 诸如S<I+1>的类型名称就不能被用来演绎I。 另外, 我们也不能通过匹配诸如int(&)[sizeof(S<T>)]类型的参数来演绎T。

具有这些约束是很正常的, 因为通常而言, 尽管有时候会很容易地忽略受限的类型名称, 但演绎过程并不是唯一的(甚至不一定是有限的) 。 而且, 一个不能演绎的上下文并没有自动地表明: 所对应的程序就是错误的, 或者前面分析的参数不能再次进行类型演绎。 为了说明这一点, 让我们考虑下面这个稍微复杂些的例子:

template <int N>
class X 
{
public:typedef int I;
	   void f(int) {
	   }
};

template<int N>
void fppm(void (X<N>::*p)(typename X<N>::I));

int main()
{
	fppm(&X<33>::f); //正确: N被演绎成33
}

在函数模板fppm()中, 子构造X<N>::I是一个不可演绎的上下文。然而, 具有成员指针类型(即X<N>::*p) 的成员类型部分X<N>是一个可以演绎的上下文。 于是, 可以根据这个可演绎上下文获得参数N, 然后把N放入不可演绎上下文X<N>::I, 就能够获得一个和实参&X<33>::f匹配的类型。 因此基于这个实参-参数对的演绎是成功的。

 

11.3 特殊的演绎情况(比较晦涩,不理解)

存在两种特殊情况, 其中用于演绎的实参-参数对(A, P) 并不是分别来自于函数调用的实参和函数模板的参数。 第1种情况出现在取函数模板地址的时候。 在这种情况下, P是函数模板声明子的参数化类型(即下面的 f 的类型) , 而 A 是被赋值(或者初始化) 的指针(即下面的pf) 所代表的函数类型。 例如:

template<typename T>
void f(T, T);
void (*pf)(char,char) = &f;

在上面的代码中, P就是void(T, T), 而A是void(char,char)。 用char替换T, 该演绎过程是成功的。 另外, pf被初始化为“特化f<char>”的地址。

另一种特殊情况和转型运算符模板一起出现。 例如:

class S {
public:
template<typename T, int N> operator T[N]&();
};

在这种情况下, 实参-参数对(A, P) 涉及到我们试图进行转型的实参和转型运算符的返回类型。 下面的代码清楚地说明了这种情况:

void f(int (&)[20]);
void g(S s)
{
    f(s);
}

在此, 我们试图把S转型为int (&)[20]; 因此, 类型A为int[20], 而类型P为T[N]。 于是, 用类型int替换T, 用20替换N之后, 该演绎就是成功的。

 

11.4 可接受的实参转型

通常, 模板演绎过程会试图找到函数模板参数的一个匹配, 以使参数化类型P等同于类型A。 然而, 当找不到这种匹配的时候, 下面的几种变化就是可接受的:

•如果原来声明的参数是一个引用参数子, 那么被替换的P类型可以比A类型多一个const或者volatile限定符。

•如果 A 类型是指针类型或者成员指针类型, 那么它可以进行限定符转型(就是说, 添加const或者volatile限定符) , 转化为被替换的P类型。

•当演绎过程不涉及到转型运算符模板的时候, 被替换的P类型可以是A类型的基类; 或者当A是指针类型时, P可以是一个指针类型, 它所指向的类型是A所指向的类型的基类。 见下面的例子:

template<typename T>
class B {
};

template<typename T>
class D : public B < T > {
};

template<typename T> 
void f(B<T>*);

void g(D<long> dl)
{
	f(&dl); //成功演绎: 用long替换T
}

只有在精确匹配不存在的情况下, 才会出现这种宽松的匹配。 即使这样, 只有在前面添加的几种转型中能够找到一种替换, 并且借助这种替换可以匹配A类型和P类型时, 演绎过程才能是成功的。

 

11.5 类模板参数

模板实参演绎只能应用于函数模板和成员函数模板,是不能应用于类模板的。另外,对于类模板的构造函数,也不能根据实参来演绎类模板参数。例如:

template<typename T>
class S {
public:
    S(T b) : a(b) {
}
    private:
    T a;
};

S x(12);   //错误:不能从构造函数的调用实参12演绎类模板参数T

 

11.6 缺省调用实参

 和普通函数一样,在函数模板中也可以指定缺省的函数调用实参。例如:

template<typename T>
void init(T* loc, T const& val = T() )
{
    *loc = val;
}

如例子所示,缺省调用实参是可以依赖于模板参数的。但是,只有在没有提供显式实参的情况下,才会实例化这种依赖型的缺省实参——这也是使得下面例子有效的一条规则:

class S {
public:
    S(int, int);
};

S s(0,0);

int main()
{
    init(&s, S(7,42) ); //因为T=S,所以T()就是无效的了。于是
                        //缺省调用实参T()也就不需要进行实例化
                        //因为已经提供了一个显式参数
}

对于缺省调用实参而言,即使不是依赖型的,也不能用于演绎模板实参。这意味着下面的C++程序是无效的:

template<typename T>
void f(T x = 42){
}

int main()
{
    f<int>();    //正确:T = int
    f();       //错误:不能根据缺省调用实参42来演绎T
}

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值