Effective Modern C++翻译(2)-条款1

第一章 类型推导

C++98有一套单一的类型推导的规则:用来推导函数模板,C++11轻微的修改了这些规则并且增加了两个,一个用于auto,一个用于decltype,接着C++14扩展了auto和decltype可以使用的语境,类型推导的普遍应用将程序员从必须拼写那些显然的,多余的类型的暴政中解放了出来,它使得C++开发的软件更有弹性,因为在某处改变一个类型会自动的通过类型推导传播到其他的地方。

 

然而,它可能使产生的代码更难观察,因为编译器推导出的类型可能不像我们想的那样显而易见。

 

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

 

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

 

条款1 明白模板类型推导

据说模仿是最诚恳的恭维之道,但是充满喜悦的无知也同样是可以衷心赞美的,当使用一个复杂的系统,忽视了它的系统是如何设计的,是如何工作的,然而对它的所完成的事情你依旧会感到很高兴,通过这种方式,C++中模板的类型推导成为了一个巨大的成功,数百万的程序员向模板函数中传递参数,并获得完全令人满意的答案,尽管很多程序员被紧紧逼着的去付出比对这些函数是如何被推导的一个朦胧的描述要更多。(even though many of those programmers would be hard-pressed to give more than the haziest description of how the types used by those functions were deduced.)

 

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

 

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

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

函数调用像下面这样

f(expr); // 用一些表达式调用f

 

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

template<typename T>
void f(const T& param); //在这个例子中, ParamType的类型是是const T&
int x = 0;
f(x);                   // 用一个int类型调用函数f

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

 

我们很自然的去期待推导出的T的类型和传递给函数实参的类型是一致的,例如,T的类型就是expr的类型,在上面的例子中,就是这种情况,x的类型是int,T被推导为int,当时它并不总是这样是的,被推导出的T的类型,不仅仅取决于expr的类型,同样取决于ParaType的形式,总共有三种情形;

  • ParamType是一个指针或是一个引用,但不是一个万能引用(universal reference),(universal reference会在条款26中进行描述,现在你只要知道他是存在的即可)。
  • ParamType是一个万能引用(universal reference) 。
  • ParamType既不是一个指针也不是一个引用 。

 

 

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

template<typename T>
void f(ParamType param);
f(expr); // 从expr中推导出T和ParamType的类型

第一种情况:ParamType是一个指针或是一个引用,但不是一个万能引用(universal reference)

最简单的情况是当ParamType是一个指针或是一个引用,但不是一个万能引用(universal reference),在这种情况下,模型推导的方式会像下面这样:

  • 如果expr的类型是一个引用,忽略引用的符号
  • 通过模式匹配expr的类型来决定ParamType的类型从而决定T的类型(Pattern-match expr‘s type against ParamType to determine T)

 

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

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

我们有这样的变量声明

int x = 27;         // x是int类型
const int cx = x;   // cx是const int
const int& rx = x;  // rx是x的一个常量引用(rx is a read-only view of 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&

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

 

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

 

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

 

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

template<typename T>
void f(const T& param); // param现在是一个指向常量的引用
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&

像之前一样,rs的引用性(reference-ness)在类型推导时被忽略了。

 

如果param是一个指针(或是一个常量指针(point to const))而不是一个引用,规则依旧适用

template<typename T>
void f(T* param);     // param现在是一个指针
int x = 27;           // 和之前一样
const int *px = &x;   // px是x的一个常量引用(rx is a read-only view of x)
f(&x);                // T是int, param的类型是int*
f(px);                // T是const int,
                      // param的类型是const int*,

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

 

第二种情况:ParamType是一个万能的引用(Universal Reference)

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

  • 如果expr是一个左值,T和ParamType都被推导为一个左值的引用
  • 如果expr是一个右值,使用通常情况下的类型推导规则

 

例如

template<typename T>
void f(T&& param);     // param现在是一个万能引用(universal reference)
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是一个lvalue, 所以T是const int&,
                       // param的类型也是const int&
f(27);                 // 27是一个rvalue, 所以T是int,
                       // param类型是int&&

条款26精确的介绍了为什么这些例子会是这样,但关键是类型推导对于模板的参数是万能引用(univsersal references)和参数是左值或右值时规则是不同的,当使用万能引用(univsersal references)的时候,类型推导规则会区别左值和右值,而这从来不会发生在非万能(例如,普通)的引用上。

 

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

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

template<typename T>
void f(T param);    // param现在是按值传递的

这意味着param将会是传递过来的对象的一个拷贝,一个全新的对象,事实上,param是一个全新的对象控制导出了T从expr中推导的规则

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

 

因此

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

注意到即使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是按值传递的
const char* const ptr ="Fun with pointers";
                       // ptr是一个指向常量对象的常量指针ptr is const pointer to const object
f(ptr);                // 实参类型是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的类型是
                                         // const char[13]
const char * ptrToName = name;      // 指向数组的指针

这里,const *char的指针ptrToName被name实例化,而name的类型是const char[13],一个13个元素的常量数组,二者的类型(const char*和const char[13])是不同的,但是因为存在数组到指针间的退化规则,上面的代码是可以通过编译的。

 

但是如果数组通过传值的方式传递给一个模板的时候,会发生什么呢?

template<typename T>
void f(T param);      // 模板是参数是按值传递的

f(name);              // T的推导结果是?

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

void myFunc(int param[]);

但是这个数组的声明是被按照一个指针的声明而对待的,这意味着myFunc和下面的声明是等价的

void myFunc(int* param); // 和上面的函数是一样的

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

 

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

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],是的,这个语法看起来是有毒的(looks toxic),但是从有利的方面看,知道这些将会奖励你那些别人得不到的罕见的分数(knowing it will score you mondo points with those rare souls who care)。

 

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

template<typename T, std::size_t N>        // 在编译期间
constexpr std::size_t arraySize(T (&)[N])  // 返回一个数组
{                                          // 的大小
return N;                                  // N是一个常量
}

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

int keyVals[] = { 1, 3, 7, 9, 11, 22, 35 }; // keyVals有
                                            // 7元素
int mappedVals[arraySize(keyVals)];         // mappedVals
                                            // 也是这样

当然,作为一个现代C++的开发人员,你应该很自然的使用std::array而不是内置的数组

std::array<int, arraySize(keyVals)> mappedVals; // mappedVals‘
                                                // 大小是7

函数参数

数组不是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);               // 参数被推导为指向函数的指针
                            // 类型是void (*)(int, double)
f2(someFunc);               // 参数被推导为指向函数的引用
                            // 类型是void (&)(int, double)

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

 

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

 

请记住:

  • 当模板的参数是一个指针或是一个引用,但不是一个万能引用(universal reference)时,实例化的表达式是否是一个引用将被忽略。
  • 当模板的参数是万能引用(universal reference)时,左值的实参产生左值的引用,右值的实参产生右值的引用。
  • 模板的参数是按值传递的时候,实例化的表达式的引用性和常量性将被忽略。
  • 在类型推导期间,数组和函数将退化为指针类型,除非他们是被实例化为相应的引用。
时间: 2024-10-11 02:16:39

Effective Modern C++翻译(2)-条款1的相关文章

Effective Modern C++翻译(5)-条款4

条款4:了解如何观察推导出的类型 那些想要知道编译器推导出的类型的人通常分为两种,第一种是实用主义者,他们的动力通常来自于软件产生的问题(例如他们还在调试解决中),他们利用编译器进行寻找,并相信这个能帮他们找到问题的源头(they're looking for insights into compilation that can help them identify the source of the problem.).另一种是经验主义者,他们探索条款1-3所描述的推导规则,并且从大量的推导情

Effective Modern C++翻译(3)-条款2

条款2 明白auto类型推导 如果你已经读完了条款1中有关模板类型推导的内容,那么你几乎已经知道了所有关于auto类型推导的事情,因为除了一个古怪的例外,auto的类型推导规则和模板的类型推导规则是一样的,但是为什么会这样呢?模板的类型推导涉及了模板,函数和参数,但是auto的类型推导却没有涉及其中的任何一个. 这确实是对的,但这无关紧要,在auto类型推导和template之间存在一个直接的映射,可以逐字逐句的将一个转化为另外一个. 在条款1中,模板类型推导是以下面的模板形式进行举例讲解的:

Effective Modern C++翻译(7)-条款6:当auto推导出意外的类型时,使用显式的类型初始化语义

条款6:当auto推导出意外的类型时,使用显式的类型初始化语义 条款5解释了使用auto来声明变量比使用精确的类型声明多了了很多的技术优势,但有的时候,当你想要zag的时候,auto可能会推导出了zig.例如,我有一个函数,它以const Widget&作为参数,并且返回std::vector<bool>,每一个bool暗示了Widget是否提供了一个特殊的特性. std::vector<bool> features(const Widget& w); 进一步假设第

Effective Modern C++翻译(4)-条款3

条款3 了解decltype decltype是一个有趣的东西,给它一个变量名或是一个表达式,decltype会告诉你这个变量名或是这个表达式的类型,通常,告诉你的结果和你预测的是一样的,但是偶尔的结果也会让你挠头思考,开始找一些参考资料进行研究,或是在网上寻找答案.   我们从典型的例子开始,因为它的结果都是在我们预料之中的,和模板类型推导与auto类型推导相比(参见条款1和条款2),decltype几乎总是总是返回变量名或是表达式的类型而不会进行任何的修改 const int i = 0;

决定干点事儿--翻译一下《effective modern c++》

写了很多关于C++11的博客,总是觉得不踏实,很多东西都是东拼西凑.市场上也很少有C++11的优秀书籍,但幸运的是Meyers老爷子并没有闲赋,为我们带来了<effective modern c++>. 我们都要认清,一个人很难超越自我,超越自我的巅峰之作.因为不同的时代,也会早就不同的伟大作品. 说上面这段话的意思就是,我们不能期待<effective modern c++>能达到<effective c++>给我们带来的惊喜,但是也是出自大师之手. Learn ho

Effective Modern C++ 读书笔记 Item 1

最近发现了<Effective Modern C++>这本书,作者正是大名鼎鼎的Scott Meyers——<Effective C++>.<Effective STL>的作者. 而就在C++11逐渐普及,甚至是C++14的新特性也进入大家的视野的时候,<Effective Modern C++>一书应运而生.此书与其前辈一样,通过数十个条款来展开,只不过这次是集中于C++11和C++14的新特性.auto.decltype.move.lambda表达式……

[C++11] Effective Modern C++ 读书笔记

本文记录了我读Effective Modern C++时自己的一些理解和心得. item1:模板类型推导 1)reference属性不能通过传值参数传入模板函数.这就意味着如果模板函数需要一个reference类型的参数,必须在模板声明中将其声明为reference,否则,即使使用一个reference类型的变量调用模板函数,类型推导的结果将不带reference属性. 2)constant和volatile属性也不能通过传值参数传入模板函数,但是可以通过reference参数传入这些属性. 3

Effective C++_笔记_条款08_别让异常逃离析构函数

(整理自Effctive C++,转载请注明.整理者:华科小涛@http://www.cnblogs.com/hust-ghtao/) C++并不禁止析构函数吐出异常,但它不鼓励你这样做.考虑如下代码: 1: class Widget{ 2: public: 3: ... 4: ~Widget() {...} //假设这个可能吐出一个异常 5: }; 6:  7: void doSomething() 8: { 9: vector<Widget> v ; //v在这里被自动销毁 10: ...

Effective C++_笔记_条款12_复制对象时勿忘其每一个成分

(整理自Effctive C++,转载请注明.整理者:华科小涛@http://www.cnblogs.com/hust-ghtao/) 编译器会在必要时候为我们的classes创建copying函数,这些“编译器生成版”的行为:将被烤对象的所有成员变量都做一份拷贝. 如果你声明自己的copying函数,意思就是告诉编译器你并不喜欢缺省实现中的某些行为.编译器仿佛被冒犯似的,会以一种奇怪的方式回敬:当你的实现代码几乎必然出错时却不告诉你.所以自己实现copying函数时,请遵循一条规则:如果你为c