条款1:理解模板类型推断

来源:互联网 发布:网页设计软件哪个好 编辑:程序博客网 时间:2024/05/22 06:59

声明:本文翻译自《Effective Modern C++》,自己边看边翻译的,不保证与英文原版完全字字对应,纯粹以学习为目的,请勿转载和用于商业用途。

如果一个复杂系统的用户对系统的工作原理一无所知,但是却能很愉悦地操作它,这说明系统设计的非常好。从这个角度看,C++的模板类型推断是极其成功的。数百万的程序员曾将参数传给模板函数,并且执行结果完全符合预期,但是很多人对于传递给这些函数的参数是如何进行类型推断的几乎一无所知。

如果你属于他们中的医院,我要告诉你一个好消息和一个坏消息。好消息是:现代C++最引人注目的功能之一:auto,是模板类型推断的基石。如果你对C++98标准的模板类型推断得心应手,那么你将对C++11标准中auto的类型推断同样信手拈来。坏消息是:当将模板类型推断法则用于auto上下文时,并没有像用于模板中那么直观。因此,真正理解auto模板类型推断的本质极其重要。本条款包含你需要知道的内容。

如果你愿意瞄一眼伪代码片段,考虑如下的函数模板:

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

可以这样调用函数:

f(expr);       // 调用f

在编译期间,编译器利用expr推断两种类型:分别是TParamType。这两种类型通常是不同的,因为ParamType一般包含修饰符,如const或者引用限定符。举个例子,如果模板声明如下:

template<typename T>void f(const T& param);  // ParamType is const T&

如果我们这样调用:

int x = 0;f(x);         // 利用int值调用

T被推断为int类型,但是ParamType被推断为const int&类型。

我们当然期望传递给函数的参数类型和T的推断类型完全一样,即Texpr类型的。对于上面的例子,情况就是这样:xint类型,T也被推断为int。但是这并不总是正确的,T的类型推断并不仅仅依赖于expr的类型,还跟ParamType的形式有关,有三种可能的情形:

  • ParamType是指针或引用类型,但是不是万能引用(Universal reference,万能引用将在条款24中介绍,目前,你只需要知道它存在,并且和左值引用、右值引用不一样。)
  • ParamType是万能引用
  • ParamType既不是指针,也不是引用

因而,我们需要开了这三种类型推断。每一种读基于以下的同样模板类型和调用方式:

template<typename T>void f(ParamType param);f(expr);    // 从expr推断T和ParamType

情形1 ParamType是引用或者指针,但不是万能引用

最简单的情形就是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的引用类型

以如下不同的调用方式推断paramT的类型:

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 int&。这对于调用者来说很重要,当向引用参数传递const对象时,该对象仍然是不可修改的,即参数是const引用类型。这说明了将const对象传递给带有T&参数的模板是安全的,因为对象的const属性是T推断类型的一部分。

注意到在第三种调用方式中,尽管rx是引用类型的,但T被推断为非引用类型,这是因为rx的引用在类型推断中被忽略了。

以上示例都是针对的左值引用,但对于右值引用参数规则实际上完全一样的。当然,只有右值参数才能传递给右值引用参数,这个限制跟类型推断无关。

我们我能将f的参数类型从T&修改为const T&,情况就不太一样了,但并不用太吃惊。cxrxconst属性仍然保留,但是由于我们假定paramconst引用,因此const不再是T推断类型的一部分:

template<typename T>void f(const T& param); // param是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&

如前所述,rx的引用属性在类型推断中被忽略了。

如果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*

此时此刻,你也许在不停的摇头、打哈欠,因为C++的类型推断准则对于引用和指针参数是如此的显而易见,这完全就是你期望的类型推断系统啊!

情形2 ParamType是万能引用类型

对于万能引用类型的参数,情形就没有那么显而易见了。万能引用跟右值引用的声明方式类似(对于包含类型参数T的函数模板,万能引用的类型声明为T&&),但是但传递左值时其行为完全不同。条款24有详细的介绍,这里只给个摘要性介绍:

  • 如果expr是一个左值,则T和ParamType都将被推断为左值引用。这在两方面是不寻常的:首先,这是模板类型推断中唯一将T推断为引用的情形;其次,尽管ParamType使用了右值引用的声明方式,但其推断类型是左值引用
  • 如果expr是一个右值,则普通准则(即情形1描述的准则)同样适用

例如:

template<typename T>void f(T&& param);   // param是万能引用int x = 27;  //保持不变const int cx = x;  // 保持不变const int& rx = x; // 保持不变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&&类型

条款24确切地解释了为什么是这样的。这里需要关注的是:万能引用参数的类型推断准则和左值及右值引用参数的推断准则不同。特别的,在使用万能引用时,类型推断会区分左值参数和右值参数,但非万能引用则不会。

情形3 ParamType既非指针,也非引用

但ParamType既非指针,也非引用时,我们将其视为传值处理:

template<typename T>void f(T param);    // param为传值参数

这意味着param是传递过来对象的拷贝,一个全新的对象。param为全新对象的事实形成了如下准则,该准则决定了T如何从expr中推断:

  1. 如前所述,若expr为引用类型,则忽略引用;
  2. 如果忽略引用属性后,exprconst类型的,继续忽略const属性。如果exprvolatile的,也忽略。(volatile对象并不常见,通常仅用于实现设备驱动,详见条款40)。

因此:

int x = 27;  // 保持不变const int cx = x;  // 保持不变const int& rx = x; // 保持不变f(x);   // T和param都是int类型f(cx);  // T和param也都是int类型f(rx);  // T和param还是int类型

注意:尽管cxrx都是const值,但是param不是const的。这是有道理的,因为paramcxrx是完全独立的对象,它只是cxrx的拷贝。cxrx不能修改并不表示param也不能修改。这就是exprconst属性(以及volatile)在param类型推断中被忽略的原因:expr不能修改并不意味着其拷贝也不能修改。

只有传值传递参数时,constvolatile才能被忽略,意识到这一点非常重要。如我们所知,对于const引用或者指向const的指针参数,exprconst属性在类型推断中仍然保留。但如果expr是指向const对象的const指针,并且expr通过传值传递给param,情形如下:

template<typanem T>void f(T param);   // param通过传值传递const char* const ptr =  "Fun with pointers";   // ptr是指向const对象的const指针f(ptr);     // 传递const char* const类型的参数

这里,星号右侧的const声明了ptrconst指针:即ptr不能改变指向,也不能被置为null。(星号左侧的const表明ptr指向的对象,是const的,不能修改)。当ptr传递给f时,实际上是将指针按字节拷贝至param。照此推断,指针ptr本身是通过传值传递的,根据传值传递的类型推断准则,指针的const属性被忽略,param的类型被推断为const char*,即指向const字符串的可变指针。当将ptr拷贝构造新指针param时,ptr指针所指对象的const属性在推断中被保留了,但是指针ptr自身的const属性被忽略了。

数组参数

以上讨论已经涵盖了主流模板类型推断的大部分,但是还有一类情形值得了解一下。尽管有时数组和指针类型可以互相转换,但是他们是不同的类型。产生错误认知的主要原因在于:在很多上线问中,数组退化为指向第一个元素的指针,这种退化使得代码像这样编译:

const char name[] = "J. P. Briggs"; //const char[13]类型const char * ptrToName = name;  // 数组退化为指针

这里,使用name初始化const char*指针ptrToName,为nameconst char[13]类型。const char*const char[13]并不一样,但是基于数组-指针退化准则,代码能通过编译。

(未完待续)

0 0
原创粉丝点击