这里对C++的类型推导方式进行一次全面的总结。
C++中有三种类型推导的方式,分别是模板、auto以及decltype()。以下分别介绍这三种方式的同异。
假设有这样的函数模板和这样的调用:
template<typename T>
void f(ParamType param);
f(expr); //使用表达式调用f
T
的类型推导不仅取决于expr
的类型,也取决于ParamType
的类型。下面份三种情况讨论这个事情。
在这种情况下,类型推导会这样进行:
expr
的类型是一个引用,忽略引用部分expr
的类型与ParamType
进行模式匹配来决定T
template<typename T>
void f(T& param); //param是一个引用
int x=27; //x是int
const int cx=x; //cx是const int
const int& rx=x; //rx是指向作为const int的x的引用
//不同的调用中,对param和T推导的类型会是这样
f(x); //T是int,param的类型是int&
f(cx); //T是const int,param的类型是const int&
f(rx); //T是const int,param的类型是const int&
在第二个和第三个调用中,注意因为cx
和rx
被指定为const
值,所以T
被推导为const int
。
因为他们传递一个const
对象给一个引用类型的形参时,他们期望对象保持不可改变性,也就是说,形参是reference-to-const
的。这也是为什么将一个const
对象传递给以T&
类型为形参的模板安全的:对象的常量性const
ness会被保留为T
的一部分。
在第三个例子中,注意即使rx
的类型是一个引用,T
也会被推导为一个非引用 ,因为rx
的引用性(reference-ness)在类型推导中会被忽略。
如果我们将f
的形参类型T&
改为const T&
,情况有所变化:
template<typename T>
void f(const T& param); //param现在是reference-to-const
int x = 27; //如之前一样
const int cx = x; //如之前一样
const int& rx = x; //如之前一样
f(x); //T是int,param的类型是const int&
f(cx); //T是int,param的类型是const int&
f(rx); //T是int,param的类型是const int&
cx
和rx
的const
ness依然被遵守,但是因为现在我们假设param
是reference-to-const
,const
不再被推导为T
的一部分。
如果param
是一个指针(或者指向const
的指针)而不是引用,情况本质上也一样:
template<typename T>
void f(T* param); //param现在是指针
int x = 27; //同之前一样
const int *px = &x; //px是指向作为const int的x的指针
f(&x); //T是int,param的类型是int*
f(px); //T是const int,param的类型是const int*
其推导规则如下:
expr
是左值,T
和ParamType
都会被推导为左值引用。这非常不寻常,第一,这是模板类型推导中唯一一种T
被推导为引用的情况。第二,虽然ParamType
被声明为右值引用类型,但是最后推导的结果是左值引用。expr
是右值,就使用正常的(也就是情景一)推导规则。template<typename T>
void f(T&& param); //param现在是一个通用引用类型
int x=27; //如之前一样
const int cx=x; //如之前一样
const int & rx=cx; //如之前一样
f(x); //x是左值,所以T是int&,
//param类型也是int&
f(cx); //cx是左值,所以T是const int&,
//param类型也是const int&
f(rx); //rx是左值,所以T是const int&,
//param类型也是const int&
f(27); //27是右值,所以T是int,
//param类型就是int&&
当ParamType
既不是指针也不是引用时,我们通过传值(pass-by-value)的方式处理,这意味着无论传递什么param
都会成为它的一份拷贝——一个完整的新对象。事实上param
成为一个新对象这一行为会影响T
如何从expr
中推导出结果。
expr
的类型是一个引用,忽略这个引用部分expr
的引用性(reference-ness)之后,expr
是一个const
,那就再忽略const
。如果它是volatile
,也忽略volatile
示例如下:
template<typename T>
void f(T param); //以传值的方式处理param
int x=27; //如之前一样
const int cx=x; //如之前一样
const int & rx=cx; //如之前一样
f(x); //T和param的类型都是int
f(cx); //T和param的类型都是int
f(rx); //T和param的类型都是int
注意即使cx
和rx
表示const
值,param
也不是const
。这是有意义的。param
是一个完全独立于cx
和rx
的对象——是cx
或rx
的一个拷贝。
但是考虑这样的情况, expr
是一个const
指针,指向const
对象,expr
通过传值传递给param
:
template<typename T>
void f(T param); //仍然以传值的方式处理param
const char* const ptr = //ptr是一个常量指针,指向常量对象
"Fun with pointers";
f(ptr); //传递const char * const类型的实参
在这里,解引用符号 * 的右边的const
表示ptr
本身是一个const
:ptr
不能被修改为指向其它地址,也不能被设置为null(解引用符号左边的const
表示ptr
指向一个字符串,这个字符串是const
,因此字符串不能被修改)。当ptr
作为实参传给f
,组成这个指针的每一比特都被拷贝进param
。像这种情况,ptr
自身的值会被传给形参,根据类型推导的第三条规则,ptr
自身的常量性const
ness将会被省略,所以param
是const char*
,也就是一个可变指针指向const
字符串。在类型推导中,这个指针指向的数据的常量性const
ness将会被保留,但是当拷贝ptr
来创造一个新指针param
时,ptr
自身的常量性const
ness将会被忽略。
1 数组实参
如果将一个数组传值给一个模板,会发生什么?
template<typename T>
void f(T param); //传值形参的模板
f(name); //T和param会推导成什么类型?
这里有一个函数的形参是数组,但是数组声明会被视作指针声明,这意味着下面的两个声明是等价的:
void myFunc(int param[]);
void myFunc(int* param); //与上面相同的函数
因为数组形参会视作指针形参,所以传值给模板的一个数组类型会被推导为一个指针类型。这意味着在模板函数f
的调用中,它的类型形参T
会被推导为const char*
:
const char name[] = "J. P. Briggs"; //name的类型是const char[13]
f(name); //name是一个数组,但是T被推导为const char*
但是现在难题来了,虽然函数不能声明形参为真正的数组,但是可以接受指向数组的引用!所以我们修改f
为传引用, 然后调用它:
template<typename T>
void f(T& param); //传引用形参的模板
f(name); //传数组给f
T
被推导为了真正的数组!这个类型包括了数组的大小,在这个例子中T
被推导为const char[13]
,f
的形参(对这个数组的引用)的类型则为const char (&)[13]
。是的,这种语法看起来简直有毒,但是知道它将会让你在关心这些问题的人的提问中获得大神的称号。
2 函数实参
在C++中不只是数组会退化为指针,函数类型也会退化为一个函数指针,我们对于数组类型推导的全部讨论都可以应用到函数类型推导和退化为函数指针上来。结果是:
void someFunc(int, double); //someFunc是一个函数,
//类型是void(int, double)
template<typename T>
void f1(T param); //传值给f1
template<typename T>
void f2(T & param); //传引用给f2
f1(someFunc); //param被推导为指向函数的指针,
//类型是void(*)(int, double)
f2(someFunc); //param被推导为指向函数的引用,
//类型是void(&)(int, double)
auto
是建立在模板类型推导的基础上的,这里举出它们的同异点。
auto
类型推导和模板类型推导有一个直接的映射关系,比如以下模板:
template<typename T>
void f(ParmaType param);
f(expr); //使用一些表达式调用f
当一个变量使用auto
进行声明时,auto
扮演了模板中T
的角色,变量的类型说明符扮演了ParamType
的角色。
比如以下示例:
auto x = 27; //这里的x的类型说明符是auto自己
const auto cx = x; //这里的x的类型说明符是const auto
const auto & rx=cx; //这里的x的类型说明符是const auto &
因此Item1描述的三个情景稍作修改就能适用于auto:
auto
类型推导和模板类型推导几乎一样的工作,它们就像一个硬币的两面,除了一个例外。下面来说说这个例外。
auto
类型推导和模板类型推导的真正区别在于,auto
类型推导假定花括号表示std::initializer_list
而模板类型推导不会这样(确切的说是不知道怎么办)。
auto x1 = 27; //类型是int,值是27
auto x2(27); //同上
auto x3 = { 27 }; //类型是std::initializer_list,值是{ 27 }
auto x4{ 27 }; //同上
template<typename T> //带有与x的声明等价的
void f(T param); //形参声明的模板
f({ 11, 23, 9 }); //错误!不能推导出T
然而如果在模板中指定T
是std::initializer_list
而留下未知T
,模板类型推导就能正常工作:
template<typename T>
void f(std::initializer_list<T> initList);
f({ 11, 23, 9 }); //T被推导为int,initList的类型为
//std::initializer_list
C++14允许auto
用于函数返回值并会被推导(参见Item3),而且C++14的lambda函数也允许在形参声明中使用auto
。但是在这些情况下auto
实际上使用模板类型推导的那一套规则在工作,而不是auto
类型推导,所以说下面这样的代码不会通过编译:
auto createInitList()
{
return { 1, 2, 3 }; //错误!不能推导{ 1, 2, 3 }的类型
}
std::vector<int> v;
…
auto resetV =
[&v](const auto& newValue){ v = newValue; }; //C++14
…
resetV({ 1, 2, 3 }); //错误!不能推导{ 1, 2, 3 }的类型
decltype
总是不加修改的产生变量或者表达式的类型。
对于T
类型的不是单纯的变量名的左值表达式,decltype
总是产出T
的引用即T&
。
C++14支持decltype(auto)
,就像auto
一样,推导出类型,但是它使用decltype
的规则进行推导。
以下说明decltype的一个重要的用法:
有如下函数:
template<typename Container, typename Index> //可以工作,
auto authAndAccess(Container& c, Index i) //但是需要改良
{
authenticateUser();
return c[i];
}
std::deque<int> d;
…
authAndAccess(d, 5) = 10; //认证用户,返回d[5],
//然后把10赋值给它
//无法通过编译器!
对一个T
类型的容器使用operator[]
通常会返回一个T&
对象,比如std::deque
就是这样。(但是std::vector
有一个例外,对于std::vector
,operator[]
不会返回bool&
,它会返回一个全新的对象, 这是一个例外)。
函数返回类型中使用auto
,编译器实际上是使用的模板类型推导的那套规则。如果那样的话这里就会有一些问题。正如我们之前讨论的,operator[]
对于大多数T
类型的容器会返回一个T&
,但是在模板类型推导期间,表达式的引用性(reference-ness)会被忽略。基于这样的规则,在这里d[5]
本该返回一个int&
,但是模板类型推导会剥去引用的部分,因此产生了int
返回类型。函数返回的那个int
是一个右值,上面的代码尝试把10赋值给右值int
,C++11禁止这样做,所以代码无法编译。
要想让authAndAccess
像我们期待的那样工作,我们需要使用decltype
类型推导来推导它的返回值,C++期望在某些情况下当类型被暗示时需要使用decltype
类型推导的规则,C++14通过使用decltype(auto)
说明符使得这成为可能。
template<typename Container, typename Index> //最终的C++14版本
decltype(auto)
authAndAccess(Container&& c, Index i)
{
authenticateUser();
return std::forward<Container>(c)[i];
}
decltype(auto)
的使用不仅仅局限于函数返回类型,当你想对初始化表达式使用decltype
推导的规则,你也可以使用:
Widget w;
const Widget& cw = w;
auto myWidget1 = cw; //auto类型推导
//myWidget1的类型为Widget
decltype(auto) myWidget2 = cw; //decltype类型推导
//myWidget2的类型是const Widget&
以上是C++类型推导的全部内容。充分使用C++的类型推导,能使我们尽可能简单的代码。