码迷,mamicode.com
首页 > 编程语言 > 详细

《Effective Modern C++》翻译--条款1: 理解模板类型推导

时间:2016-01-10 11:50:32      阅读:252      评论:0      收藏:0      [点我收藏+]

标签:

北京2016年1月9日13:47:17 开始第一章的翻译。
第一章名为 类型推断
分为四个条款:
1理解模板类型推导
2理解auto自动类型推导
3理解decltype操作符
4如何对待推导的类型

第一章 类型推导

C++98有一套单一的类型推导的规则用来推导函数模板。C++11轻微的修改了这些规则并且增加了两个推导规则,一个用于auto,一个用于decltype。接着C++14扩展了auto和decltype可以使用的语境。类型推导的普遍应用将程序员从必须拼写那些显然多余的类型中解放了出来,它使得C++开发的软件更有弹性,因为在某处改变一个类型会自动的通过类型推导传播到其他的地方。它使C ++软件适应性更强,因为改变一个类型在源代码中一个点自动通过类型推演到其他地方传播。但是,它可以使代码更难推理,因为编译器推断该类型可能不像我们想象的那么明显。

想要在现代C++中进行有效率的编程,你必须对类型推导操作有一个扎实的了解。因为有太多的情形你会用到它:在函数模板的调用中;在auto出现的大多数场景中;在decltype表达式中;在C++14中在神秘的decltype(auto)构造被应用的时候。

这一章提供了一些每一个C++开发者都需要了解的关于类型推导的基本信息,它解释了模板类型推导是如何工作的,auto是如何在此基础上建立自己的规则的,decltype是如何按自己的独立的规则工作的,它甚至解释了你如何强迫编译器来使类型推导的结果可见,从而让你确定编译器的结果是你想要的。

条款1: 理解模板类型推导
有人说,模仿是最真诚的奉承形式,但幸福的无知可以是同样由衷的赞誉。当使用者使用一个复杂的系统,忽视了它的系统是如何设计的,是如何工作的,然而对它的所完成的事情你依旧会感到很高兴,通过这种方式,C++中模板的类型推导成为了一个巨大的成功,数百万的程序员向模板函数中传递参数,并获得完全令人满意的答案,尽管很多程序员被紧紧逼着的去付出比对这些函数是如何被推导的一个朦胧的描述要更多。

如果上面提到的人群中包括你,我有一个好消息和一个坏消息。好消息是对于auto声明的变量的类型推导规则和模板在本质上是一样的,所以当涉及到auto的时候,你会感到很熟悉(见条款2)。坏消息是当模板类型推导的规则应用到auto的时候,你很可能对发生的事情感到惊讶,如果你想要使用auto(相比精确的类型声明,你当然更应该使用auto,见条款5),你需要对模板类型推导规则有一个合理正确的认识,他们通常是直截了当的,所以这不会照成太大的挑战,和在C++98里工作的方式是一样的,你很可能不需要对此有过多的思考。

如果你愿意忽略少量的伪代码,我们可以直接对下面的模板函数的代码进行思考:

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

可以这样调用:

f(expr);  //call f with some expression

在编译时期,编译器根据expr来推导两个类型:一个是T的,一个是ParamType的。这两个类型经常是不同的,因为ParamType经常包括例如const或者是引用的限定的一些修饰符。例如,如果模板像下面这样声明:

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

我们这样调用:

int x = 0;
f(x); //call f with an int

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

很自然人们期待类型推导T与传递给函数的参数的类型相同,就像T是expr类型一样。在上面的例子中,就是这样,x是int类型,而T被推导为int类型。但是情况并不总是这样。T被推导的类型不仅仅取决于expr,也与ParamType类型有关。有三种情况:

?ParamType是一个指针或是引用类型,但不是一个universal reference(universal reference在条款26中进行阐述。目前,你只要知道universal reference的存在就可以了)。

?ParamType是一个universal reference。

?ParamType既不是指针也不是引用。

因此,我们会有三种类型推导的情景,每一个调用都会以我们通用的模板形式为基础:

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

f(expr);   //deduce T and ParamType from expr

第一种情况:ParamType是一个指针或是引用类型,但不是一个universal reference

最简单的情况就是当ParamType是一个指针或是引用类型,但不是universal reference的时候。在这样的情况下,类型推导是这样工作的:

?如果expr是引用类型,则忽略引用部分。

?通过模式匹配expr的类型来决定ParamType的类型从而决定T的类型

例如,如果我们的模板是这样的:

template<typename T>
void f(T& param);  //param是一个引用

并且,我们如下声明变量:

int x = 27;        //x is an int
const int cx = x;  //cx is a const int
const int& rx = x; //rx is a read-only view of x

函数调用时,推导出的Param和T的类型如下:

f(x);              //T is int, param‘s type is int&

f(cx);             //T is const int, param‘s type is const int&

f(rx);             //T is const int, param‘s type is const int&

在第二个和第三个函数调用中,注意到因为cx和rx被指派为const了,T被推导为const int,因此产生的参数类型是const int&。这对调用者来说是十分重要的。当他们向一个引用类型的参数传递一个const对象时,他们期待这个对象依旧是无法被修改的,比如,这个参数的类型被推导为一个指向const的引用。这就是为什么向带有一个T&参数的模板传递一个const对象是安全的,对象的常量性成为了推导出的类型T的一部分。

在第三个例子中,注意到尽管rs是一个引用类型,T被推导为一个非引用类型,这是因为rs的引用性(reference-ness)在推导的过程中被忽略了,如果不是这样的话(例如,T被推导为const int&),param的类型将会是const int&&,一个引用的引用,引用的引用在C++里是不允许的,避免他们的唯一方法在类型推导时忽略表达式的引用性。

这些例子都是左值的引用参数,但是这些类型推导规则对于右值的引用参数同时适用,当然,只有右值的实参会被传递给一个右值类型的引用,但是这对类型推导没有什么影响。

如果我们把f的参数类型由T&改成const T&,事情会发送一点小小的改变,但不会太让人惊讶。cx和rx的常量性依旧满足,但是因为我们现在假定了param是一个常量的引用,const不在需要被推导为T的一部分了:

template<typename T>
void f(const T& param); //param is now a ref-to-const

int x = 27;             //as before
const int cx = x;       //as before
const int& rx = x;      //as before

f(x);                   //T is int, param‘s type is const int&

f(cx);                   //T is int, param‘s type is const int&

f(rx);                   //T is int, param‘s type is const int&

和前面的一样,在类型推导的时候rx的引用性被忽略了。

如果param是一个指针类型(或者是指向常量的指针)而不是引用类型,还是按照同样的方式进行类型推导。

template<typename T>
void f(T* param); //param is now a pointer

int x = 27;             //as before
const int *px = &x;     //px is a ptr to a read-only view of x

f(&x);                   //T is int, param‘s type is int*

f(px);                   //T is const int, param‘s type is const int*

此时此刻,你可能发现你自己在不断的打哈欠和点头,因为C++的类型推导规则对于引用和指针类型的参数是如此的平常,看见他们一个个被写出来是一件很枯燥的事情。因为他们是如此的显而易见,和你在类型推导中期待的是一样的。

第二种情况:ParamType是一个universal reference

当涉及到universal reference作为模板的参数的时候(例如 T&&参数),事情变得不是那么清楚了,因为规则对于左值参数有着特殊的对待。完整的内容将在条款26中讲述,但这里有一个概要的版本:

?如果expr是一个左值,T和ParamType都被推导为一个左值的引用

?如果expr是一个右值,使用通常情况下的类型推导规则

例如:

template<typename T>
void f(T&& param);      //param is now a universal reference

int x = 27;             //as before
const int cx = x;       //as before
const int& rx = x;      //as before
f(x);                   //x is lvalue, so T is int&, param‘s type is also int&

f(cx);                  //cx is lvalue, so T is const int&, 
                        //param‘s type is also const int&

f(rx);                  //rx is lvalue, so T is const int&,
                        //param‘s type is also const int&

f(27);                  //27 is rvalue, so T is int, param‘s type is therefore int&&

条款26详细解释了为什么,但现在重要的是类型推导对于模板的参数是univsersal references和参数是左值或右值时规则是不同的。当使用univsersal references的时候,类型推导规则会区别左值和右值,而这从来不会发生在nivsersal references的引用上。

第三种情况:ParamType既不是指针也不是引用

当ParamType既不是指针也不是引用的时候,我们按照按值传递进行处理:

template<typename T>
void f(T param);     //param is now passed by value

这意味着无聊传递的是什么,param都将成为它的一个拷贝–完全一个新的对象。事实上,param是一个全新的对象控制导出了T从expr中推导的规则:

?和之前一样,如果expr的类型为引用,那么将忽略引用部分。

?如果在expr的引用性被忽略之后,expr带有const修饰,忽略const,如果带有volatile修饰,同样忽略(volatile对象是不寻常的对象,他们通常仅被用来实现设备驱动程序,更多的细节,可以参照条款42)。

所以:

int x = 27;             //as before
const int cx = x;       //as before
const int& rx = x;      //as before

f(x);                   //T and param are both int

f(cx);                  //T and param are again both int

f(rx);                  //T and param are still both int

注意,尽管cx和rx代表的是常量,但param不是常量。这很有意义。因为parm是和cx,rx完全独立的对象,它是cx和rx的一个拷贝。事实上cx和rx不能被修改和param是否能被修改没有任何的关系,这就是为什么expr的常量性在推导param类型的时候被忽略了;因为expr不能被修改并不意味着它的拷贝也不能被修改。

注意到const仅仅在按值传递的参数中被忽略掉是很重要的。正如我们看到的那样,对于指向常量的引用和指针来说,expr的常量性在类型推导的时候是被保留的。但是考虑下面的情况,expr是一个指向const对象的常量指针,并且expr按值传递给一个参数:

template<typename T>
void f(T param);                             //param is still passed by value

const char* const ptr = "Fun with pointers"; //ptr is const pointer to const object

f(ptr);                                      //pass arg of type const char* const

这里,乘号右侧的const将ptr声明为const,意思是ptr不能指向一个不同的位置,也不能把它设为null(乘号左侧的const指ptr指向的字符串是const,因此字符串不能被修改)。当ptr别传递给f的时候,指针按位拷贝给param。因此,指针本身(ptr)将是按值传递的,根据按值传递的类型推导规则,ptr的常量性将被忽略,param的类型被推导为const char*,一个可以修改所指位置的指针,但指向的字符串是不能修改的。ptr所指的常量性在类型推导的时候被保留了下来,但是ptr本身的常量性在通过拷贝创建新的指针param的时候被忽略掉了。

数组作为参数

这几乎涵盖了它的主流模板类型推导,但有一个侧流情况下是值得了解。尽管数组类型和指针类型有时可以互换, 但二者还是不同的。这种错觉的一个主要贡献者是,在许多情况下,一个数组退化成一个指向它的第一个元素。这种退化是允许这样的代码进行编译:

const char name[] = "J. P. Briggs"; //name‘s type is const char[13]

const char* ptrToName = name;       //array decays to pointer

这里,常量类型的字符串指针ptrToName被初始化为name,而name是一个常量数组,也就相当于是具有十三个常量字符元素的数组。这些类型(const char*和const char[13])是不一样的,但是由于数组到指针的退化,这样的代码是可以编译通过的。

但是,如果模板的参数是一个按值传递的数组类型呢?那又会发生什么呢?

template<typename T>
void f(T param);       //template with by-value parameter

f(name);               //what types are deduced for T and param?

我们首先应该注意到函数的参数中是不存在数组类型的参数的,是的,下面的语法是合法的

void myFunc(int param[]);

但是,此时的数组声明被看着与指针声明一样,这就意味着函数myFunc可以这样等价声明为:

void myFunc(int* param); //same function as above

数组和指针在参数上的等价源于C++是以C为基础创建的,它产生了数组和指针在类型上是等价的这一错觉。

因为数组参数的声明被按照指针的声明而对待,通过按值的方式传递给一个模板参数的数组将被推导为一个指针类型,这意味着在下面这个模板函数f的调用中,参数T的类型被推导为const char*:

f(name);                //name is array, but T deduced as const char*

但是现在来了一个曲线球,尽管函数不能声明一个真正意义上的数组类型的参数,但是他们可以声明一个指向数组的引用,所以如果我们把模板f改成按引用传递参数:

template<typename T>
void f(T& param);     //template with by-reference paremeter

然后 ,我们给他传递一个数组:

f(name);              //pass array to f

T的类型被推导为数组的类型。这个类型包括了数组的大小,所以在上面这个例子中,T被推导为const char[13],f的参数的类型(对数组的一个引用)是const char(&)[13]。对的,这个语法看起来是有害的,但是从好的的方面看,知道这些将会奖励你那些别人得不到的罕见的分数(知道这些对你有好处)。

有趣的是,声明一个指向数组的引用能够让我们创建一个模板来返回数组的长度,也就是数组具有的元素个数:

template<typename T, std::size_t N>       //return size of
constexpr std::size_t arraySize(T (&)[N]) //an array as a
{                                         //compile-time
   return N;                              //constant
}

注意到constexpr的使用(参见条款14)让函数的结果在编译期间就可以获得,这就可以让我们声明一个数组的长度和另一个数组的长度一样

int keyVals[] = { 1, 3, 7, 9, 11, 22, 35 }; // keyVals has
                                            // 7 elements
int mappedVals[arraySize(keyVals)];         // so does 
                                            // mappedVals

当然,作为一个现代C++的开发者,你应该更习惯的使用std::array而不是内置的数组:

std::array<int, arraySize(keyVals)> mappedVals; // mappedVals‘
                                                // size is 7

函数作为参数

在C++中,数组不是唯一能够退化成指针的实体。函数类型同样可以退化成函数指针,并且我们讨论的任何一个关于类型推导的规则和对数组相关的事情对于函数的类型推导也适用,函数类型会退化为函数的指针。因此:

void someFunc(int, double); // someFunc is a function;
                            // type is void(int, double)
template<typename T>
void f1(T param);           // in f1, param passed by value

template<typename T>
void f2(T& param);          // in f2, param passed by ref

f1(someFunc);               // param deduced as ptr-to-func;                        
                            // type is void(*)(int, double)

f2(someFunc);               // param deduced as ref-to-func;
                            // type is void(&)(int, double)

这和数组实际上并没有什么不同。但是如果你想学习数组到指针的退化 ,你还是应该同时了解一下函数到指针退化比较好。

所以,到这里你应该知道了模板类型推导的规则,在最开始的时候我就说他们是如此的简单明了。事实上,对于大多数规则而言,也确实是这样的,唯一可能会激起点水花的是在使用universal references时,左值有着特殊的待遇,甚至数组和函数到指针的退化规则会让水变得浑浊。有时,你可能只是简单的抓住你的编译器,”告诉我,你推导出的类型是什么“。当这种情况发生时,转向条款4,因为它是专门用于哄骗编译器将这样做。

请记住:

?当模板的参数是指针或是引用,但不是universal reference时,初始化的表达式是否是一个引用将被忽略。

?当模板的参数是universal reference时,左值的实参产生左值的引用,右值的实参产生右值的引用。

?模板的参数是按值传递的时候,实例化的表达式的引用性和常量性将被忽略。

?在类型推导期间,数组和函数将退化为指针类型,除非他们是被初始化化为引用。

《Effective Modern C++》翻译--条款1: 理解模板类型推导

标签:

原文地址:http://blog.csdn.net/wangshubo1989/article/details/50487774

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!