C++类型推导

这里对C++的类型推导方式进行一次全面的总结。

C++中有三种类型推导的方式,分别是模板、auto以及decltype()。以下分别介绍这三种方式的同异。

一 模板

假设有这样的函数模板和这样的调用:

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

f(expr);                        //使用表达式调用f

T的类型推导不仅取决于expr的类型,也取决于ParamType的类型。下面份三种情况讨论这个事情。

1 ParamType是一个指针或引用,但不是通用引用

在这种情况下,类型推导会这样进行:

  1. 如果expr的类型是一个引用,忽略引用部分
  2. 然后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&

在第二个和第三个调用中,注意因为cxrx被指定为const值,所以T被推导为const int

因为他们传递一个const对象给一个引用类型的形参时,他们期望对象保持不可改变性,也就是说,形参是reference-to-const的。这也是为什么将一个const对象传递给以T&类型为形参的模板安全的:对象的常量性constness会被保留为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&

cxrxconstness依然被遵守,但是因为现在我们假设param是reference-to-constconst不再被推导为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*

2 ParamType是一个通用引用

其推导规则如下:

  • 如果expr是左值,TParamType都会被推导为左值引用。这非常不寻常,第一,这是模板类型推导中唯一一种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&&

3 ParamType既不是指针也不是引用

ParamType既不是指针也不是引用时,我们通过传值(pass-by-value)的方式处理,这意味着无论传递什么param都会成为它的一份拷贝——一个完整的新对象。事实上param成为一个新对象这一行为会影响T如何从expr中推导出结果。

  1. 和之前一样,如果expr的类型是一个引用,忽略这个引用部分
  2. 如果忽略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

注意即使cxrx表示const值,param也不是const。这是有意义的。param是一个完全独立于cxrx的对象——是cxrx的一个拷贝。
但是考虑这样的情况, 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本身是一个constptr不能被修改为指向其它地址,也不能被设置为null(解引用符号左边的const表示ptr指向一个字符串,这个字符串是const,因此字符串不能被修改)。当ptr作为实参传给f,组成这个指针的每一比特都被拷贝进param。像这种情况,ptr自身的值会被传给形参,根据类型推导的第三条规则,ptr自身的常量性constness将会被省略,所以paramconst char*,也就是一个可变指针指向const字符串。在类型推导中,这个指针指向的数据的常量性constness将会被保留,但是当拷贝ptr来创造一个新指针param时,ptr自身的常量性constness将会被忽略。

两个特殊情况

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是建立在模板类型推导的基础上的,这里举出它们的同异点。

1 相同点

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类型推导和模板类型推导几乎一样的工作,它们就像一个硬币的两面,除了一个例外。下面来说说这个例外。

2 不同点

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

然而如果在模板中指定Tstd::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++14resetV({ 1, 2, 3 });            //错误!不能推导{ 1, 2, 3 }的类型

三 decltype

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::vectoroperator[]不会返回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++的类型推导,能使我们尽可能简单的代码。

你可能感兴趣的:(C++,c++)