本文是对《Effective Modern C++》第一章型别推导的总结。
针对模板推导过程如下:
template <typename T>
void func(ParamType param);
func(expr);
上面的伪代码是一个函数模板,我们通过实参expr对模板形参T以及函数形参ParamType进行推导。根据ParamType的三种情况,推导情况各不相同。
情况1: ParamType是个指针或引用,但不是个万能引用
推导规则如下:
- 若expr具有引用型别,先将引用部分忽略
- 尔后,对expr的型别和ParamType的型别执行模式匹配,来决定T的型别。
template<typename T>
void func(T& param);
int x = 0;
const int cx = x;
const int& rx = x;
func(x);//T的型别是int,ParamType的型别是int&
func(cx);//T的型别是const int,ParamType的型别是const int&
func(rx);//先去掉引用,然后T的型别是const int,ParamType的型别是const int&
template<typename T>
void func(const T& param);
int x = 0;
const int cx = x;
const int& rx = x;
func(x);//T的型别是int,ParamType的型别是const int&
func(cx);//T的型别是int,ParamType的型别是const int&
func(rx);//先去掉引用,然后T的型别是int,ParamType的型别是const int&
template<typename T>
void func(T* param);
int x = 0;
const int *px = &x;
f(&x);//T的型别int,ParamType的型别是int*
f(px);//T的型别是const int,ParmaType的型别是const int*
情况2:ParamType是个万能引用
首先要确定什么情况下才是万能引用,要同时满足下面三个条件:
- 严格按照格式T&&声明(不一定非要用T这个字母)
- T需要进行型别推导
- 不包含任何型别修饰符比如const或者volatile
template<typename T>
void func(T&& param);//是万能引用
auto&& var = var1;//是万能引用
int&& i = k//右值引用,因为不需要型别推导
template<typename T>
void func(std::vector<T>&& param);右值引用,因为vector必须给定一个指定型别,比如vector<int>,T不需要被推导
template<typename T>
void func(const T&& param);//是右值引用,因为有const修饰符
tempate<typename T>
class vector {
template<class..Args>
void emplace_back(Args&&... args);
};//Args&&是万能引用,需要型别推导,严格按照T&&格式,没有const等修饰符
- 如果expr是个左值,T和ParamType都会被推导为左值引用。
- 如果expr是个右值,则应按照情况1中的规则确定。
template<typname T>
void func(T&& param)//万能引用
int x = 0;
const int cx = x;
const int& rx = x;
func(x);//x是左值,T和ParamType都是int&
func(cx);//x是左值,T和ParamType都是const int&
func(rx);//x是左值,T和ParamType都是const int&
func(0);//x是右值,T是int,ParamType是int&&
情况3:ParamType既非指针也非引用
ParamType即不是指针也不是引用,那么ParamType只能是按值传递了,做为参数按值传递,形参分配的实参其实是个副本,也就是说如果实参的属性是const,但并不保证副本的属性也是const,因此副本已经保证实参不会被修改了。
- 一如之前,若expr是具有引用的型别,则忽略其引用的部分
- 忽略expr的引用性之后,同时也忽略const及volatile属性
template<typename T>
void func(T param);
int x = 0;
const int cx = x;
const int& rx = x;
const int* const px = &x;
func(x);//T和ParamType都是int
func(cx);//T和ParamType都是int
func(rx);//T和ParamType都是int
func(px);//参数复制的是指针的副本,所以int* const中的const被忽略,但是const int的属性被保留,所以T和ParamType都是const int*
数组实参
在C/C++语言中,数组类型可以被退化成指针比如:
const char name[] = "A";
const char* pName = name;
因此如果模板函数的实参是一个数组时,其型别会被推导成一个指针,但这还要看模板的形参是怎么声明的。
- 如果模板形参是值类型,那么数组实参会被推导成一个指针
- 如果模板形参是引用类型,那么数组形参会被推导成数组类型
template<typename T>
void func(T param);
const char name[] = "Hello";
func(name);//T和ParamType都是const char*
template<typename T>
void func(T& param);
const char name[] = "Hello";
func(name);//T是const char[5]而ParamType则是const char&[5]
我们可以利用数组形参的推导在编译器计算数组的大小。
template<typename T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept
{
return N;
}
int kyeVals[] = {1,2,3};
int arry[arraySize(kyeVals)];
将函数声明为constexpr,能够将返回值在编译期就可以使用。C++语言的知识点太多,太散,导致使用起来不太友好,但是同时也必须感叹一下C++编译器的强大。
除了数组会被退化为指针,函数也会被退化为函数指针
template<typename T>
void func1(T param);
template<typename T>
void func2(T& param);
void somefunc(int, double);
func1(somefunc);//param被推导为void (*)(int, double);
func2(somefunc);//param被推导为void (&)(int, double);
auto型别的推导
- auto的型别推导和模板型别推导一致,只有一个地方有区别。
- auto如果是以{}方式初始化的话,将会被推导成std::initializer_list<T>类型,但是模板无法进行{}推导,会编译出错。
auto = {1,2,3};//auto首先被推导成std::initializer_list<T>,然后再根据元素推导出T,最终的型别是std::initializer_list<int>
auto = {1,2,3.0};//编译错误,因为T无法被推导出来
template<typename T>
void func(T param);
func({1,2,3});//编译错误
template<typename T>
void func(sstd::initializer_list<T> initlist);
func({1,2,3});//ok,没问题
C++14允许使用auto进行函数返回值的推导,但是这个推导是模板推导,因此不支持{}。另外C++14中的lambda表达式可以使用auto做为形参,这个推导也是模板推导,不是auto推导。
auto somefunc()
{
return {1,2,3}//错误,模板无法推导{}
}
std::vector<int> v;
auto resetV = [&v](const auto& newValue){v = newValue;}
resetV({1,2,3});//错误auto是模板推导,不支持{}
理解decltype
decltype型别推导很简单,给定的名字或表达式是什么型别,它就返回什么型别。
Widget w;
const Widget& crw = w;
auto myWidget1 = crw;//auto型别是Widget
decltype(auto) myWidet2 = cw;//auto的型别是const Widget&
返回值型别尾序语法,返回值的类型可以使用形参进行推导。
template<typename Container, typename Index>
auto somefunc(COntainer& c, Index i)
-> decltype(c[i])
{
...
return c[i];
}//C++11版本
template<typename Container, typename Index>
decltype(auto) somefunc(Container& c, Index i)
{
...
return c[i];
}//C++14版本
//这里的Container我们假设是一个容器,通常返回容器中的元素是以引用的方式返回,这样我们就可以修改容器中的元素。
std::deque<int> d;
somefunc(d,5) = 10;
template<typename Container, typename Index>
auto somefunc(COntainer& c, Index i)
{
...
return c[i];
}//错误,auto是值传递,按照auto的型别推导会把引用去掉,所以返回的是一个右值临时变量,对于一个右值复杂,编译器会报错,正确的写法是decltype(auto),让decltype保证c[i]是什么就返回什么。
上面的函数只接受左值参数,如果是参数是一个右值怎么办,比如:
std::deque<std::string> makeStringDeque();
auto s = authAndAccess(makeStringDeque(), 5);
解决的办法是使用万能引用
template<typename Container, typename Index>
decltype(auto) somefunc(Container&& c, Index i)
{
...
return std::forward<Container>(c)[i];
}//C++14版本
template<typename Container, typename Index>
auto somefunc(Container&& c, Index i)
-> decltype(std::forward<Container>(c)[i])
{
...
return std::forward<Container>(c)[i];
}//C++11
//std::forward<Container>(c)[i];//会将左值保持为左值,右值转换右值型别,听起来很奇怪是吧,Container&& c是万能引用,它是一个形参,形参无论怎么声明都是个左值,所以需要std::forward<Container>进行完美转发,详细解释不在本章的范围,后面会写。
decltype的一个小细节
int x = 0;
decltype(x)//x是个名字,所以decltype(x)型别是int
decltype((x))//(x)是个表达式,所以decltyep((x))是int&
掌握查看型别推导结果的方法
- 利用IDE编辑器,编译器错误消息和Boost.TypeIndex库常常能够查看到推导而得的型别。
- 有些工具产生的结果可能是无用,或者不准确的。所以,理解C++型别推导规则是必要的。