Item1 理解模板类型推导
在Effective Modern C++一书中,条目1介绍了模板类型推导的一些基本规则,我现在对Item1 的内容进行一些翻译和总结。本文通过8个例子针对模板类型推导中的3种情况进行分别讨论。
如果我们忽略一些细节,则我们大体上可认为模板如下所示:
template<typename T>
void f(ParamType param)
调用格式如下所示:
f(expr)
在编译期间,编译器通常会进行两方面的类型推导,第一方面是对T进行类型推导,第二是对ParamType进行类型推导。这两个类型通常是不一样的,ParamType可同包含一些其他修饰(比如const)。例如下面的模板声明:
Template<typename T>
void f(const T& param) //此例中,ParamType的类型为const T&
在模板类型推导中,需要考虑以下3中情况:
case 1: ParamType是指针或者引用,但它不是一个综合引用(Universal Reference)
在这种情形下,类型按如下规则进行推导:
1) 如果expr是引用类型,则忽略引用(&)部分
2) 根据expr类型和ParamTypeType类型决定T的类型。(对这条规则我不太理解,我感觉应该是先对T进行类型推导,然后方可决定ParamType的类型)
例子 1:
模板定义如下所示:
template<typename T>
void f(T& param) // param 是一个引用类型
现在我们有如下变量定义:
int x = 27 ;
const int cx = x ;
const int& rx = x ;
则我们对T和param的类型推导结果如下所示:
f(x); // T is int, param's type is int&
f(cx); // T is const int,
// param's type is const int&
f(rx); // T is const int,
// param's type is const int&
在第二、三种情况中,cx、rx都为const类型,所以在对T进行类型推导时,T的类型继续保持const类型,编译器进行这样的推导是必须而且合理的,试想如果T的类型推导中不保持const属性,那在函数内部将有可能对这个变量进行某些修改,这些修改是被编译器所允许的。那将一个const(只读)类型的变量传进函数将是不安全的。
在第三种情况中rx是个引用类型,根据情形1的规则,引用部分(&)将被忽略,故T的类型被推导为const int。如果T被推导为const int&,则param的类型就会被推导为const int& & , 意为引用之引用,这在C++中是不被允许的。为了避免出现这种情况(param被推导为“引用之引用”),解决办法是忽略掉“&”而将T推导为const int。
例子2:
在例2中,我们将函数f形参类型有T&改为const T&, 如下所示:
template<typename T>
void f(const T& param); // param is now a ref-to-const
int x = 27; // as before
const int cx = x; // as before
const int& rx = x; // as before
f(x); // T is int, param's type is const int&
f(cx); // T is int, param's type is const int&
f(rx); // T is int, param's type is const int&
在例2 的第二、第三种情况中,cx、rx是const类型,由于我们假设param是一个referenc-to-const类型,所以在对T进行类型推导的时候没有必要保持const修饰了。
例子3:param是指针(pointer)或者指向常量的指针(pointer-to-const)
template<typename T>
void f(T* param); // param is now a pointer
int x = 27; // as before
const int *px = &x; // px is a ptr to a read-only view of x
f(&x); // T is int, param's type is int*
f(px); // T is const int,
// param's type is const int*
从例3中,我们可以看到,指针和引用的类型推导规则是类似的,当expr是指针类型时,对T的类型推导将忽略expr的指针修饰符(*),当param中包含常量(const)的时候,对T、param的类型推导中,将继续保留const关键字。
case 2: ParamType是一个综合引用(Universal Reference)
当ParamType是一个综合引用(即“T&&”)时,类型推导遵循以下准则:
1)如果expr是一个左值(lvalue),则对T以及ParamType的推导中,仍将他们推导为左值。
2)如果expr是一个右值(rvalue),则按通常的规则对T以及ParamType进行推导。
例子 4:
template<typename T>
void f(T&& param); // param is now a universal reference
int x = 27; // as before
const int cx = x; // as before
const int& rx = x; // as before
f(x); // x is lvalue, so T is int&,
// param's type is also int&
f(cx); // cx is lvalue, so T is const int&,
// param's type is also const int&
f(rx); // rx is lvalue, so T is const int&,
// param's type is also const int&
f(27); // 27 is rvalue, so T is int,
// param's type is therefore int&&
从例子4中,我们可以看到,如果expr是个左值,则不论expr是否是引用类型,T以及ParamType都会被推导为引用类型,如果expr是const类型,则对T以及ParamType的推导都会继续保留const修饰符。综合引用的好处是可以实现完美转化(perfect forwarding)。”Perfect Forwarding”也被翻译成完美转发,精准转发等,说的都是一个意思。
精确传递适用于这样的场景:需要将一组参数原封不动的传递给另一个函数。“原封不动”不仅仅是参数的值不变,在 C++ 中,除了参数值之外,还有一下两组属性:
左值/右值和 const/non-const。 精确传递就是在参数传递过程中,所有这些属性和参数值都不能改变。例如:
函数 forward_value 是一个泛型函数,它将一个参数传递给另一个函数 process_value。
forward_value 的定义为:
template <typename T> void forward_value(const T& val) { process_value(val); } template <typename T> void forward_value(T& val) { process_value(val); }
函数 forward_value 为每一个参数必须重载两种类型,T& 和 const T&,否则,下面四种不同类型参数的调用中就不能同时满足 :
int a = 0; const int &b = 1; forward_value(a); // int& forward_value(b); // const int& forward_value(2); // int&
对于一个参数就要重载两次,也就是函数重载的次数和参数的个数是一个正比的关系。这个函数的定义次数对于程序员来说,是非常低效的。我们看看右值引用如何帮助我们解决这个问题 :
template <typename T> void forward_value(T&& val) { process_value(val); }
只需要定义一次,接受一个右值引用的参数,就能够将所有的参数类型原封不动的传递给目标函数。四种不用类型参数的调用都能满足,参数的左右值属性和 const/non-cosnt 属性完全传递给目标函数 process_value。这个解决方案不是简洁优雅吗?
int a = 0; const int &b = 1; forward_value(a); // int& forward_value(b); // const int& forward_value(2); // int&&
在对T&&的推导中,右值实参为右值引用,左值实参仍然为左值引用。一句话,就是参数的属性不变。这样也就完美的实现了参数的完整传递。
case 3: ParamType既不是指针(pointer)也不是引用(reference)
当ParamType既不是指针也不是引用是,我们按值传递(pass-by-value)来处理。
template<typename T>
void f(T param); // param is now passed by value
也就是说在这种情况下,param是传递给函数的实参的拷贝,而不论实参是什么类型,具体来说:
1) 如果expr是个引用类型,则T的推导中将忽略引用类型
2) 如果expr是const类型,则忽略const类型,如果expr是volatile类型,仍然忽略掉
下面看一个例子:
例子 5
int x = 27; // as before
const int cx = x; // as before
const int& rx = x; // as before
f(x); // T and param are both int
f(cx); // T and param are again both int
f(rx); // T and param are still both int
在例5的三种情况中,对T、Param的推导中,去除了实参中的const、&等修饰,传进函数f的实参只是x、cx、rx的一个副本。
例子 6 expr是const-pointer-to-const-object
template<typename T>
void f(T param); // param is still passed by value
const char* const ptr = "Fun with pointers" ; // ptr is const pointer to const object
f(ptr); // pass arg of type const char * const, T is const char *
此时,传到函数中的指针只是ptr的一个副本,所以这个副本的指针的const属性在推导过程中被忽略了,但副本锁指向的内容仍然保留为const类型,故T的推导类型为const char *。
例子 7 数组类型的参数
template<typename T>
void f(T param); // template with by-value parameter
const char name[]="J. P. Briggs" ;
f(name); // what types are deduced for T and param?
数组作为参数传递到函数中时,编译器会把他作为指针来处理,这时候数组会退化为指针类型。所以任何传递到此函数的数组都会被推导为指针,也就是说此时,T被推导为const char*。
那么问题来了,如果我们想让编译器将T推导为数组类型的话,应该怎么改写模板呢?来看下面的模板声明:
template<typename T>
void f(T& param); // template with by-reference parameter
//and we pass an array to it,
f(name); // pass array to f
这时候,T就被推导为实际的数组类型了!并且这个推导包含数组的大小信息,即T被推导为const char[13]。
例子8 参数是函数
当我们想函数中传递实参的时候,数组类型的实参会退化为指针,具由这种特性的并非只有数组,函数也是其中一员。当实参是函数名字的时候,编译器会将其退化成函数指针。请看下面的例子
void someFunc(int, double); // someFunc is a function;
// type is void(int, double)
template<typename T>
void f1(T param); // in f1, param passed by value
template<typename T>
void f2(T& param); // in f2, param passed by ref
f1(someFunc); // param deduced as ptr-to-func;
// type is void (*)(int, double)
f2(someFunc); // param deduced as ref-to-func;
// type is void (&)(int, double)
从例8中,我们看到,不管是数组还是函数,当通过“pass-by-value”类型的模板时,都会退化为指针类型,如果想让编译器将T推导为数组或者函数的实际类型,则应该用“T&”类型作为模板函数的形参。