理解模板类型推导
c++的auto特性是建立在模板类型推到的基础上。坏消息是当模板类型推导规则应用于auto
环境时,有时不如应用于template时那么直观。我们可能很自然的期望T
和传递进函数的实参是相同的类型,也就是,T
为expr
的类型。但有时情况并非总是如此,T
的类型推导不仅取决于expr
的类型,也取决于ParamType
的类型。这里有三种情况:
template<typename T>
void f(ParamType param)
情景一:ParamType是一个指针或引用,但不是通用引用
- 如果expr类型是一个引用,会忽略引用部分【因为引用部分独立于T】
- 然后expr的类型与ParamType进行模式匹配来决定T
情景二:ParamType是一个通用引用(T&&)
- 如果expr是左值,T和paramType都会被推到为左值引用。第一,这是模板类型推导中唯一一种
T
被推导为引用的情况。 - 如果
expr
是右值,就使用正常的(也就是情景一)推导规则(忽略引用后于ParamType进行模式匹配来决定T)
情景三:ParamType既不是指针也不是引用
void f(T param);
- 和之前一样,如果
expr
的类型是一个引用,忽略这个引用部分 - 如果忽略
expr
的引用性(reference-ness)之后,expr
是一个const
,那就再忽略const
。如果它是volatile
,也忽略volatile
(volatile
对象不常见,它通常用于驱动程序的开发中。关于volatile
的细节请参见40)
忽略引用行在指向常量的常量指针情况下只会忽略底层const,这一特性只会对形参本身有效,因此指针的常量特定会被忽略,但是指针所指对象的常量特性会被保留。
在类型推导中,这个指针指向的数据的常量性const
ness将会被保留,但是当拷贝ptr
来创造一个新指针param
时,ptr
自身的常量性const
ness将会被忽略。
特殊情况:数组实参(函数类似)
在大多数情况下数组会被退化为指针。如果我们ParamType就是T,那么传递数组实参和传递指针形参情况类似,推断的结果是一个指针。但是如果声明为传递引用形参的模板,T被推到为真正的数组!
template<typename T>
void f(T& param);
f(name);//name是一个数组指向const char[13],T也是一个数组,且ParamType为const char(&)[13]
有趣的是,可声明指向数组的引用的能力,使得我们可以创建一个模板函数来推导出数组的大小:
//在编译期间返回一个数组大小的常量值(//数组形参没有名字,
//因为我们只关心数组的大小)
template<typename T, std::size_t N> //关于
constexpr std::size_t arraySize(T (&)[N]) noexcept //constexpr
{ //和noexcept
return N; //的信息
} //请看下面
总结
- 在模板类型推导时,有引用的实参会被视为无引用,他们的引用会被忽略
- 对于通用引用的推导,左值实参会被特殊对待
- 对于传值类型推导,
const
和/或volatile
实参会被认为是non-const
的和non-volatile
的 - 在模板类型推导时,数组名或者函数名实参会退化为指针,除非它们被用于初始化引用
理解auto类型推导
当一个变量使用auto
进行声明时,auto
扮演了模板中T
的角色,变量的类型说明符扮演了ParamType
的角色。废话少说,这里便是更直观的代码描述,考虑这个例子:
template<typename T> //概念化的模板用来推导x的类型
void func_for_x(T param);
auto x=27;
func_for_x(27); //概念化调用:
//param的推导类型是x的类型
template<typename T> //概念化的模板用来推导cx的类型
void func_for_cx(const T param);
const auto cs=x;
func_for_cx(x); //概念化调用:
//param的推导类型是cx的类型
template<typename T> //概念化的模板用来推导rx的类型
void func_for_rx(const T & param);
const auto& rx=x
func_for_rx(x); //概念化调用:
//param的推导类型是rx的类型
不同于模板类型推导的特殊情况
这就造成了auto
类型推导不同于模板类型推导的特殊情况。当用auto
声明的变量使用花括号进行初始化,auto
类型推导推出的类型则为std::initializer_list
。如果这样的一个类型不能被成功推导(比如花括号里面包含的是不同类型的变量),编译器会拒绝这样的代码:
auto x1 = 27; //类型是int,值是27
auto x2(27); //同上
auto x3 = { 27 }; //类型是std::initializer_list<int>,
//值是{ 27 }
auto x4{ 27 }; //同上
auto x5 = { 1, 2, 3.0 }; //错误!无法推导std::initializer_list<T>中的T
对于模板,无法推导花括号为std::initializer_list
template<typename T>
void f(std::initializer_list<T> initList);
f({ 11, 23, 9 }); //T被推导为int,initList的类型为
//std::initializer_list<int>
新标准:
==在c++14允许auto用于函数返回值并会被推导。==c++14也允许lambda函数在形参声明中使用auto,但是在这些情况下,auto实际使用模板类型推导的那一套规则在工作,而不是auto类型推导。所以下面这样的代码不会通过编译。
auto createInitList(){
return {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
我们将从一个简单的情况开始,没有任何令人惊讶的情况。相比模板类型推导和auto
类型推导,decltype
只是简单的返回名字或者表达式的类型(不会忽略const和引用)。decltype作用与返回左值的表达式得到的是一个引用。(我们可以为返回左值的表达式赋值)。例如decltype中表达式的内容是一个解引用(*p),返回的并不是指针所指向的对象本身,而是一个引用。
在C++11中,decltype
最主要的用途就是用于声明函数模板,而这个函数返回类型依赖于形参类型。函数名称前面的auto
不会做任何的类型推导工作。相反的,他只是暗示使用了C++11的尾置返回类型语法,即在函数形参列表后面使用一个”->
“符号指出函数的返回类型,尾置返回类型的好处是我们可以在函数返回类型中使用函数形参相关的信息。
template<typename Container, typename Index> //可以工作,
auto authAndAccess(Container& c, Index i) //但是需要改良
->decltype(c[i])
{
authenticateUser();
return c[i];
}
C++11允许自动推导单一语句的lambda表达式的返回类型, C++14扩展到允许自动推导所有的lambda表达式和函数,甚至它们内含多条语句。对于authAndAccess
来说这意味着在C++14标准下我们可以忽略尾置返回类型,只留下一个auto
。使用这种声明形式,auto标示这里会发生类型推导。更准确的说,编译器将会从函数实现中推导出函数的返回类型。
然而这么做会出现一个问题,大多数T类型的容器会返回一个T&,但是模板类型推导期期间,表达式的引用性会被忽略。要想让authAndAccess
像我们期待的那样工作,我们需要使用decltype
类型推导来推导它的返回值,即指定authAndAccess
应该返回一个和c[i]
表达式类型一样的类型。
c++14中可以使用decltype(auto)说明符使得这成为可能。实际上我们可以这样解释它的意义:auto
说明符表示这个类型将会被推导,decltype
说明decltype
的规则将会被用到这个推导过程中。因此我们可以这样写authAndAccess
:
decltype(auto)
的使用不仅仅局限于函数返回类型,当你想对初始化表达式使用decltype
推导的规则,你也可以使用:
template<typename Container, typename Index> //C++14版本,
decltype(auto) //可以工作,
authAndAccess(Container& c, Index i) //但是还需要
{ //改良
authenticateUser();
return c[i];
}
decltype(auto) muWidget2 = cw;
上述authAndAccess函数实际上还存在一些漏洞。为了使函数支持左值引用和右值引用==(这种情况很少,但向authAndAccess
传递一个临时变量也并不是没有意义,有时候用户可能只是想简单的获得临时容器中的一个元素的拷贝,比如这样)==,我们需要将声明改为通用引用(也可以重载函数)。
注意这里我们也并不知道index的类型,但我们并没有声明为引用的形式。因为就容器索引来说,我们遵照标准模板库对于索引的处理是有理由的(比如std::string
,std::vector
和std::deque
的operator[]
),所以我们坚持传值调用。
最后我们用std::forward来实现通用引用(22)
template<typename Container, typename Index> //最终的C++14版本
decltype(auto)
authAndAccess(Container&& c, Index i)
{
authenticateUser();
return std::forward<Container>(c)[i];//使用std::forward保证实参c和形参c的引用类型一致(都是左值引用或右值引用)
}
//对于c++11需要用尾置返回值
//->decltype(std::forward<Contrainer>(c)[i])
特殊情况
将decltype
应用于变量名会产生该变量名的声明类型。然而,对于比单纯的变量名更复杂的左值表达式,decltype
可以确保报告的类型始终是左值引用。也就是说,如果一个不是单纯变量名的左值表达式的类型是T
,那么decltype
会把这个表达式的类型报告为T&
。
我们回到最开始说的*p的情况,p是一个单纯变量名,如果decltype§返回的是p的声明类型,然而*p是返回左值的复杂表达式了,因此decltype返回的是左值引用,类型是表达式的类型。
总结
decltype
(单独使用或者与auto
一起用)可能会偶尔产生一些令人惊讶的结果,但那毕竟是少数情况。通常,decltype
都会产生你想要的结果,尤其是当你对一个变量使用decltype
时,因为在这种情况下,decltype
只是做一件本分之事:它产出变量的声明类型。
decltype
总是不加修改的产生变量或者表达式的类型。- 对于
T
类型的不是单纯的变量名的左值表达式,decltype
总是产出T
的引用即T&
。 - C++14支持
decltype(auto)
,就像auto
一样,推导出类型,但是它使用decltype
的规则进行推导。
学会查看类型推导结果
我们探究三种方案:在你编辑代码的时候获得类型推导的结果,在编译期间获得结果,在运行时获得结果。
IDE编辑器
在IDE中的代码编辑器通常可以显示程序代码中变量,函数,参数的类型,你只需要简单的把鼠标移到它们的上面,举个例子,有这样的代码中:
运行时输出
std::cout << typeid(x).name() << '\n'; //显示x和y的类型
std::cout << typeid(y).name() << '\n';
调用std::type_info::name
不保证返回任何有意义的东西,但是库的实现者尝试尽量使它们返回的结果有用。实现者们对于“有用”有不同的理解。举个例子,GNU和Clang环境下x
的类型会显示为”i
“,y
会显示为”PKi
“,这样的输出你必须要问问编译器实现者们才能知道他们的意义:”i
“表示”int
“,”PK
“表示”pointer to konst
const
“(指向常量的指针)。(这些编译器都提供一个工具c++filt
,解释这些“混乱的”类型)Microsoft的编译器输出得更直白一些:对于x
输出”int
“对于y
输出”int const *
“
std::type_info::name
规范批准像传值形参一样来对待这些类型。正如item1中提到的,如果传递的是一个引用,那么引用部分(reference-ness)将被忽略,如果忽略后还具有const
或者volatile
,那么常量性const
ness或者易变性volatile
ness也会被忽略。
依赖于库
在std::type_info::name
和IDE失效的地方,Boost TypeIndex库(通常写作Boost.TypeIndex)被设计成可以正常运作。这个库不是标准C++的一部分,也不是IDE或者TD
这样的模板。Boost库(可在boost.com获得)是跨平台,开源,有良好的开源协议的库,这意味着使用Boost和STL一样具有高度可移植性。
这里是如何使用Boost.TypeIndex得到f
的类型的代码
#include <boost/type_index.hpp>
template<typename T>
void f(const T& param)
{
using std::cout;
using boost::typeindex::type_id_with_cvr;
//显示T
cout << "T = "
<< type_id_with_cvr<T>().pretty_name()
<< '\n';
//显示param类型
cout << "param = "
<< type_id_with_cvr<decltype(param)>().pretty_name()
<< '\n';
}
std::vetor<Widget> createVec(); //工厂函数
const auto vw = createVec(); //使用工厂函数返回值初始化vw
if (!vw.empty()){
f(&vw[0]); //调用f
…
}
//假设vw是一个const类型,我们传递的是其中一个元素的地址(该元素也是const),因此T被推断为const(底层const保留)和*,而param 被推断为const * const&,顶层const在形参列表中写明,&在形参列表中写明。
T = Widget const *
param = Widget const * const&