第一章 型别推导

一 理解模板型别推导

函数模板类型如下:

template <typename T>
void f(ParamType param);    //ParamType是一个T相关的类型

f(expr);    //推导T和ParamType的型别

ParamType有三种类型:

  • ParamType是个指针或引用,但不是万能引用
  • ParamType是万能引用
  • ParamType不是指针也不是引用

对应代码如下:

template <typename T>
void func1(T& param)    //指针或引用,但不是万能引用
{
    cout << param << endl;
}
template <typename T>
void func2(T&& param)   //万能引用
{
    cout << param << endl;
}
template <typename T>
void func3(T param)     //不是指针也不是引用
{
    cout << param << endl;
}

int main()
{
    int x = 10;
    const int cx = x;
    const int &rx = x;
    func1(x);   //T:int; param:int &
    func1(cx);  //T:const int; param:const int &
    func1(rx);  //T:const int; param:const int &

    func2(x);   //T:int &; param:int &
    func2(cx);  //T:const int &; param:const int &
    func2(rx);  //T:const int &; param:const int &
    func2(10);  //T:int; param:int &&

    func3(x);   //都是int
    func3(cx);  //都是int
    func3(rx);  //都是int

    return 0;
}

对于func1()来说,传入参数的引用类型是被忽略的,并通过param的类型和ParamType的类型执行模式匹配,来决定T的类型。const类型成为型别推导的组成部分。即传入的是int或者是int &,T类型都是int,而param类型为int &。

对于func2()来说,既可以传入左值又可以传入右值,传入左值的时候,T和param类型是一致的,传入右值的时候,T的类型是int,param的类型就变为int &&,这是一个右值引用,延长了右值的生存时间。

对于func3()来说,传入的param是实参的一个副本,所以无论实参是什么类型,param都只传入最基本的类型,忽略const,volatile等组成部分。但是如果传入的实参指向的对象也是const的话,传入param后,这个特性是不会变的。例如const int * const p传入,指针和指针指向的对象都是不可以修改的,但是传入func3()后,指针的副本可以修改,但是指针的副本指向的对象还是不能修改。

数组实参

如果传入的实参是数组的话,有两种情况:

1.按值传递

数组会退化成指向首元素的指针,这时候类似于传入的是一个指针。

const char name[] = "123456789";
template <typename T>
void func1(T param) //按值传递
{
    cout << param << endl;
}
func1(name);    //name会从const char[]退化为const char *

2.按引用传递

函数将形参声明成数组的引用。

const char name[] = "123456789";
template <typename T>
void func1(T& param)    //指针或引用,但不是万能引用
{
    cout << param << endl;
}
func1(name);    //T的型别会被推导为实际的数组类型,类型中包含数组尺寸

上例中,T的类型是const char[10],而f的形参是const char(&)[10]。

所以我们可以使用这个模板来推导数组元素的个数

template <typename T, size_t N>
const size_t func4(T (&)[N])        //不是指针也不是引用
{
    return N;
}

const char name[] = "123456789";
const int ret = func4(name);
cout << ret << endl;    //10

函数实参

函数实参很简单,就是函数退化为函数指针(func1())和函数引用(func2())

template <typename T>
void func1(T& param)    //指针或引用,但不是万能引用
{
    cout << param << endl;
}
template <typename T>
void func2(T&& param)   //万能引用
{
    cout << param << endl;
}

void somef(int, int);
func1(somef);   //退化为函数指针void (*)(int, int)
func2(somef);   //退化为函数引用void (&)(int, int)

二 理解auto型别推导

auto关键字可以进行型别推导,auto的型别推导是类似于模板的型别推导的。

void func(int a, int b)
{
    cout << a << ' ' << b << endl;
}

template <typename T>
void f(T param) 
{
    cout << typeid(param).name() << endl;
}
template <typename T>
void f11(initializer_list<T> param)
{
    cout << typeid(T).name() << endl;
    cout << typeid(param).name() << endl;
}

int main()
{
    auto x = 27;
    const auto cx = x;
    const auto &rx = x;
    auto &&uref1 = x;   //uref1:int &
    auto &&uref2 = cx;  //uref2:const int &
    auto &&uref3 = 10;  //uref3:int &&
    const char name[] = "fuhao";
    auto arr1 = name;   //arr1:const char *
    auto &arr2 = name;  //arr2:const char (&)[5]
    auto f1 = func;     //f1:void (*)(int, int)
    cout << typeid(f1).name() << endl;
    auto &f2 = func;    //f2:void (&)(int, int)
    cout << typeid(f2).name() << endl;
    auto x1 = 10;
    auto x2(10);
    cout << typeid(x2).name() << endl;
    auto x3 = { 10 };
    cout << typeid(x3).name() << endl;
    auto x4{10};
    cout << typeid(x4).name() << endl;
    //auto x4 = { 10, 20, 5.0 };    //无法运行,原因是initializer_list中成员类型不一致的情况下,无法推导出initializer_list的类型,也就无法推导x4的类型
    auto p = {11,22,33};
    //f({ 11,22,33 });  //模板推导不能推导initializer_list类型
    f11({ 11,22,33 });

    return 0;
}

这里对x、cx、rx的型别推导就类似于模板的型别推导。但是有一种例外,auto型别推导出来的结果和模板推导是不一样的。

对于模板推导来说,无法进行initializer_list的推导,所以只能使用f11那种方式,这样T的类型被推导为int,而param的类型为initializer_list<int>。上述代码的编译器报错如下:
这里写图片描述
这里写图片描述
同样的,用auto来指定C++14中lambda表达式的形参型别时,也不能使用大括号括起来。

vector<int> v;
auto ret = &v{ v = val; };
ret({1,2,3});   //有错,无法进行类型推导

三 理解decltype

decltype可以返回给定的变量或者表达式的型别,不对其做任何修改。

例子如下:

class Widget {
public:
    Widget(int _w = 10) : w(_w) { }
    int getW() const { return w; };
private:
    int w;
};
struct point
{
    int x, y;   //decltype(x)、decltype(y):int
};
bool f(const Widget &w) decltype(w):const Widget &; decltype(f):bool(const Widget &)
{
    if (w.getW() != 0)
        return true;
    else
        return false;
}
template <typename T>
class myvector
{
public:
    T & operator[](size_t index) {
        return x;
    }
private:
    T x;
};

int main()
{
    const int i = 0;    //decltype(i):const int
    Widget w;   //decltype(w):Widget
    if (f(w)) ; //decltype(f(w)):bool
    myvector<int> v;//decltype(v):myvector<int>
    if (v[0] == 0) ;//decltype(v[0])是int

    return 0;
}

C++11中,decltype的主要用于就是声明那些返回值型别依赖于形参型别的函数模板。例如模板函数中operator[]要返回容器内部的元素,对于含有型别T的容器,operator[]一般要返回T&(std::vetor<bool>除外),decltype可以较好的实现这点。

在C++11中,可以配合使用auto和返回值型别尾序语法,即该返回值的型别在形参列表之后,用->标识出,尾序语法的好处是在指定返回值型别的时候可以可以使用形参函数。

template<typename Container, typename Index>
auto access(Container &&c, Index i)
//完美转发,将右值c转化为左值,最终返回的是内部型别的左值引用
-> decltype(forward<Container>(c)[i])   
{  }

在C++14中可以对所有lambda表达式和函数的返回值型别进行推导,所以可以去掉尾序语法,但是这样做,auto在推导返回型别的时候会去掉引用性质,如果我们想返回T&,通过auto的推导,最终返回的是T而不是T&,这样返回值是一个右值,就无法对其进行赋值了。C++14中可以使用一个新的饰词,就是decltype(auto),这样auto指定了要实施推导的型别,而推导过程采用的是decltype的规则。这样,返回值型别为:

template<typename Container, typename Index>
decltype(auto) access2(Container &&c, Index i)
{  }

这样,decltype(auto)就和函数内部返回的结果是一样的类型了,而不会去掉内部return返回值的引用饰词。
这里写图片描述
这里写图片描述
由上图可见,auto会去掉引用性质,而decltype(auto)不会。

上述两个例子的容器用的都是万能引用,这是因为传进来的容器不一定是左值,也有可能是一个右值,但是我们在函数内部已经明确return的值是一个左值引用,这样我们就要用到完美转发,无论传入的值是左值还是右值,否会返回右值引用,上述两个例子的函数体如下:

//C++11
template<typename Container, typename Index>
auto access(Container &&c, Index i)
-> decltype(forward<Container>(c)[i])
{
    return forward<Container>(c)[i];    //完美转发,无论c是左值还是右值,通通返回左值引用(如果c[i]返回的是引用的话)
}
//C++14
template<typename Container, typename Index>
decltype(auto) access2(Container &&c, Index i)
{
    return forward<Container>(c)[i];
}

要记住的一点是,千万不能用decltype(auto)返回局部变量的引用。

decltype推导的规则如下:

  1. 如果e是一个没有带括号的标记符表达式或者类成员访问表达式,那么decltype(e)就是e所命名的实体的类型,此外如果e是一个被重载的函数,那么会导致编译错误。
  2. 否则假设e的类型是T,如果e是一个将亡值,那么decltype(e)是T&&
  3. 否则假设e的类型是T,如果e是一个左值,那么decltype(e)是T&
  4. 否则,假设e的类型是T,则decltype(e)为T

标记符指的是去除关键字,字面量等编译器需要使用的标记之外的程序员自定义的标记,而单个标记符对应的表达式即为标记符表达式

int arr[4];     //arr是一个标记符,arr[1]+3就不是

例如:

int i = 10;
decltype(i) a;  //a被推导为int
decltype((i)) b = 10;   //b被推导为int &,必须对其初始化,否则无法通过编译

i是一个标记符表达式,而(i)是一个左值表达式,这样推导出来的结果必然是不同的。

#include <iostream>
using namespace std;
int main()
{
    int i = 4;
    int arr[5] = { 0 };
    int *ptr = arr;
    struct S { double d; }s;
    void Overloaded(int);
    void Overloaded(char);//重载的函数
    int && RvalRef();
    const bool Func(int);

    //规则一:推导为其类型
    decltype (arr) var1; //int [5]
    decltype (ptr) var2;//int *
    decltype(s.d) var3;//doubel 成员访问表达式
    //decltype(Overloaded) var4;//重载函数。编译错误。

    //规则二:将亡值。推导为类型的右值引用。
    decltype (RvalRef()) var5 = 1;

    //规则三:左值,推导为类型的引用。
    decltype ((i))var6 = i;     //int&
    decltype (true ? i : i) var7 = i; //int&  条件表达式返回左值
    decltype (++i) var8 = i; //int&。
    decltype(arr[5]) var9 = i;//int&
    decltype(*ptr) var10 = i;//int& *操作返回左值
    decltype("hello") var11 = "hello"; //const char(&)[6]  字符串字面常量为左值,且为const左值。

    //规则四:以上都不是,则推导为本类型
    decltype(1) var12;//int
    decltype(Func(1)) var13 = true;//bool
    decltype(i++) var14 = i;//int i++返回右值
    var12 = 10;
    var13 = false;

    return 0;
}

P.S. C++11标准库中的模板类is_lvalue_reference来判断表达式是否为左值。

cout << is_lvalue_reference<decltype(++i)>::value << endl;

结果是true表示左值,false表示右值。

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

一般情况下可以通过IDE(我用的是VS2017社区版)和编译器的报错信息,知道型别是什么,这里不展开来说了。

这里主要说一下运行时的输出。

一般情况下可以通过typeid返回的type_info中的type_info::name()来输出输出成员信息,但是这样的输出不一定是对的。因为type_info::name()在返回的时候会去除掉const、volatile的饰词信息,也回去除引用的信息,这样无论是const int还是int &,或者是const/volatile int &都只返回int。

//1
int x = 10;
const type_info &obj = typeid(x);
cout << obj.name();
//2
cout << typeid(x).name() << endl;

还可以使用boost::typeindex::type_id_with_cvr::pretty_name()来返回正确的型别。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值