Modern C++ 型别推导

本文是对《Effective Modern C++》第一章型别推导的总结。

针对模板推导过程如下:

template <typename T>
void func(ParamType param);

func(expr);

上面的伪代码是一个函数模板,我们通过实参expr对模板形参T以及函数形参ParamType进行推导。根据ParamType的三种情况,推导情况各不相同。

情况1: ParamType是个指针或引用,但不是个万能引用

推导规则如下:

  1. 若expr具有引用型别,先将引用部分忽略
  2. 尔后,对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是个万能引用

首先要确定什么情况下才是万能引用,要同时满足下面三个条件:

  1. 严格按照格式T&&声明(不一定非要用T这个字母)
  2. T需要进行型别推导
  3. 不包含任何型别修饰符比如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等修饰符
  1. 如果expr是个左值,T和ParamType都会被推导为左值引用。
  2. 如果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,因此副本已经保证实参不会被修改了。

  1. 一如之前,若expr是具有引用的型别,则忽略其引用的部分
  2. 忽略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;

因此如果模板函数的实参是一个数组时,其型别会被推导成一个指针,但这还要看模板的形参是怎么声明的。

  1. 如果模板形参是值类型,那么数组实参会被推导成一个指针
  2. 如果模板形参是引用类型,那么数组形参会被推导成数组类型
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型别的推导

  1. auto的型别推导和模板型别推导一致,只有一个地方有区别。
  2. 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&

掌握查看型别推导结果的方法

  1. 利用IDE编辑器,编译器错误消息和Boost.TypeIndex库常常能够查看到推导而得的型别。
  2. 有些工具产生的结果可能是无用,或者不准确的。所以,理解C++型别推导规则是必要的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值