diff --git a/7.TheConcurrencyAPI/item38.html b/7.TheConcurrencyAPI/item38.html index 9e393065..9133da4c 100644 --- a/7.TheConcurrencyAPI/item38.html +++ b/7.TheConcurrencyAPI/item38.html @@ -188,7 +188,7 @@
std::thread t(std::move(pt)); //在t上运行pt
-这个例子是你对于future的析构函数的正常行为有一些了解,但是将这些语句放在一个作用域的语句块里更容易看:
+这个例子使你对于future的析构函数的正常行为有一些了解,但是将这些语句放在一个作用域的语句块里会使其更易于阅读:
{ //开始代码块
std::packaged_task<int()>
pt(calcValue);
diff --git a/print.html b/print.html
index e96d4bf9..53ee5597 100644
--- a/print.html
+++ b/print.html
@@ -5389,7 +5389,7 @@ Item23):
std::thread t(std::move(pt)); //在t上运行pt
-
这个例子是你对于future的析构函数的正常行为有一些了解,但是将这些语句放在一个作用域的语句块里更容易看:
+这个例子使你对于future的析构函数的正常行为有一些了解,但是将这些语句放在一个作用域的语句块里会使其更易于阅读:
{ //开始代码块
std::packaged_task<int()>
pt(calcValue);
diff --git a/searchindex.js b/searchindex.js
index d90502e4..8c029d7b 100644
--- a/searchindex.js
+++ b/searchindex.js
@@ -1 +1 @@
-Object.assign(window.search, {"doc_urls":["Introduction.html#简介","Introduction.html#术语和惯例","Introduction.html#报告bug提出改进意见","1.DeducingTypes/item1.html#第1章-类型推导","1.DeducingTypes/item1.html#条款一理解模板类型推导","1.DeducingTypes/item1.html#情景一paramtype是一个指针或引用但不是通用引用","1.DeducingTypes/item1.html#情景二paramtype是一个通用引用","1.DeducingTypes/item1.html#情景三paramtype既不是指针也不是引用","1.DeducingTypes/item1.html#数组实参","1.DeducingTypes/item1.html#函数实参","1.DeducingTypes/item2.html#条款二理解auto类型推导","1.DeducingTypes/item3.html#条款三理解decltype","1.DeducingTypes/item4.html#条款四学会查看类型推导结果","1.DeducingTypes/item4.html#ide编辑器","1.DeducingTypes/item4.html#编译器诊断","1.DeducingTypes/item4.html#运行时输出","2.Auto/item5.html#第2章-auto","2.Auto/item5.html#条款五优先考虑auto而非显式类型声明","2.Auto/item6.html#条款六auto推导若非己愿使用显式类型初始化惯用法","3.MovingToModernCpp/item7.html#第3章-移步现代c","3.MovingToModernCpp/item7.html#条款七区别使用和创建对象","3.MovingToModernCpp/item8.html#条款八优先考虑nullptr而非0和null","3.MovingToModernCpp/item9.html#条款九优先考虑别名声明而非typedefs","3.MovingToModernCpp/item10.html#条款十优先考虑限域enum而非未限域enum","3.MovingToModernCpp/item11.html#条款十一优先考虑使用-deleted-函数而非使用未定义的私有声明","3.MovingToModernCpp/item12.html#条款十二使用override声明重写函数","3.MovingToModernCpp/item13.html#条款十三优先考虑const_iterator而非iterator","3.MovingToModernCpp/item14.html#条款十四如果函数不抛出异常请使用noexcept","3.MovingToModernCpp/item15.html#条款十五尽可能的使用constexpr","3.MovingToModernCpp/item16.html#条款十六让const成员函数线程安全","3.MovingToModernCpp/item17.html#条款十七理解特殊成员函数的生成","4.SmartPointers/item18.html#第4章-智能指针","4.SmartPointers/item18.html#条款十八对于独占资源使用stdunique_ptr","4.SmartPointers/item19.html#条款十九对于共享资源使用stdshared_ptr","4.SmartPointers/item20.html#条款二十当stdshared_ptr可能悬空时使用stdweak_ptr","4.SmartPointers/item21.html#条款二十一优先考虑使用stdmake_unique和stdmake_shared而非直接使用new","4.SmartPointers/item22.html#条款二十二当使用pimpl惯用法请在实现文件中定义特殊成员函数","5.RRefMovSemPerfForw/item23.html#第5章-右值引用移动语义完美转发","5.RRefMovSemPerfForw/item23.html#条款二十三理解stdmove和stdforward","5.RRefMovSemPerfForw/item24.html#条款二十四区分通用引用与右值引用","5.RRefMovSemPerfForw/item25.html#条款二十五对右值引用使用stdmove对通用引用使用stdforward","5.RRefMovSemPerfForw/item26.html#条款二十六避免在通用引用上重载","5.RRefMovSemPerfForw/item27.html#条款二十七熟悉通用引用重载的替代方法","5.RRefMovSemPerfForw/item27.html#放弃重载","5.RRefMovSemPerfForw/item27.html#传递const-t","5.RRefMovSemPerfForw/item27.html#传值","5.RRefMovSemPerfForw/item27.html#使用-tag-dispatch","5.RRefMovSemPerfForw/item27.html#约束使用通用引用的模板","5.RRefMovSemPerfForw/item27.html#折中","5.RRefMovSemPerfForw/item28.html#条款二十八理解引用折叠","5.RRefMovSemPerfForw/item29.html#条款二十九假定移动操作不存在成本高未被使用","5.RRefMovSemPerfForw/item30.html#条款三十熟悉完美转发失败的情况","5.RRefMovSemPerfForw/item30.html#花括号初始化器","5.RRefMovSemPerfForw/item30.html#0或者null作为空指针","5.RRefMovSemPerfForw/item30.html#仅有声明的整型static-const数据成员","5.RRefMovSemPerfForw/item30.html#重载函数的名称和模板名称","5.RRefMovSemPerfForw/item30.html#位域","5.RRefMovSemPerfForw/item30.html#总结","6.LambdaExpressions/item31.html#第6章--lambda-表达式","6.LambdaExpressions/item31.html#条款三十一避免使用默认捕获模式","6.LambdaExpressions/item32.html#条款三十二使用初始化捕获来移动对象到闭包中","6.LambdaExpressions/item33.html#条款三十三对auto形参使用decltype以stdforward它们","6.LambdaExpressions/item34.html#条款三十四考虑-lambda-而非stdbind","7.TheConcurrencyAPI/Item35.html#第7章-并发api","7.TheConcurrencyAPI/Item35.html#条款三十五优先考虑基于任务的编程而非基于线程的编程","7.TheConcurrencyAPI/item36.html#条款三十六如果有异步的必要请指定stdlaunchasync","7.TheConcurrencyAPI/item37.html#条款三十七使stdthread在所有路径最后都不可结合","7.TheConcurrencyAPI/item38.html#条款三十八关注不同线程句柄的析构行为","7.TheConcurrencyAPI/item39.html#条款三十九对于一次性事件通信考虑使用void的-futures","7.TheConcurrencyAPI/item40.html#条款四十对于并发使用stdatomic对于特殊内存使用volatile","8.Tweaks/item41.html#第8章-微调","8.Tweaks/item41.html#条款四十一对于移动成本低且总是被拷贝的可拷贝形参考虑按值传递","8.Tweaks/item42.html#条款四十二考虑使用置入代替插入"],"index":{"documentStore":{"docInfo":{"0":{"body":16,"breadcrumbs":0,"title":0},"1":{"body":147,"breadcrumbs":0,"title":0},"10":{"body":259,"breadcrumbs":3,"title":1},"11":{"body":236,"breadcrumbs":3,"title":1},"12":{"body":6,"breadcrumbs":2,"title":0},"13":{"body":15,"breadcrumbs":3,"title":1},"14":{"body":45,"breadcrumbs":2,"title":0},"15":{"body":154,"breadcrumbs":2,"title":0},"16":{"body":5,"breadcrumbs":5,"title":2},"17":{"body":190,"breadcrumbs":4,"title":1},"18":{"body":171,"breadcrumbs":4,"title":1},"19":{"body":15,"breadcrumbs":5,"title":2},"2":{"body":5,"breadcrumbs":1,"title":1},"20":{"body":308,"breadcrumbs":3,"title":0},"21":{"body":146,"breadcrumbs":4,"title":1},"22":{"body":205,"breadcrumbs":4,"title":1},"23":{"body":312,"breadcrumbs":4,"title":1},"24":{"body":187,"breadcrumbs":4,"title":1},"25":{"body":266,"breadcrumbs":4,"title":1},"26":{"body":123,"breadcrumbs":4,"title":1},"27":{"body":147,"breadcrumbs":4,"title":1},"28":{"body":290,"breadcrumbs":4,"title":1},"29":{"body":235,"breadcrumbs":4,"title":1},"3":{"body":7,"breadcrumbs":3,"title":1},"30":{"body":139,"breadcrumbs":3,"title":0},"31":{"body":20,"breadcrumbs":3,"title":1},"32":{"body":195,"breadcrumbs":3,"title":1},"33":{"body":197,"breadcrumbs":3,"title":1},"34":{"body":103,"breadcrumbs":3,"title":1},"35":{"body":233,"breadcrumbs":3,"title":1},"36":{"body":327,"breadcrumbs":3,"title":1},"37":{"body":22,"breadcrumbs":3,"title":1},"38":{"body":173,"breadcrumbs":3,"title":1},"39":{"body":195,"breadcrumbs":2,"title":0},"4":{"body":43,"breadcrumbs":2,"title":0},"40":{"body":249,"breadcrumbs":3,"title":1},"41":{"body":189,"breadcrumbs":2,"title":0},"42":{"body":10,"breadcrumbs":2,"title":0},"43":{"body":2,"breadcrumbs":2,"title":0},"44":{"body":4,"breadcrumbs":4,"title":2},"45":{"body":21,"breadcrumbs":2,"title":0},"46":{"body":90,"breadcrumbs":4,"title":2},"47":{"body":171,"breadcrumbs":2,"title":0},"48":{"body":55,"breadcrumbs":2,"title":0},"49":{"body":180,"breadcrumbs":2,"title":0},"5":{"body":96,"breadcrumbs":3,"title":1},"50":{"body":49,"breadcrumbs":2,"title":0},"51":{"body":42,"breadcrumbs":2,"title":0},"52":{"body":51,"breadcrumbs":2,"title":0},"53":{"body":3,"breadcrumbs":3,"title":1},"54":{"body":40,"breadcrumbs":4,"title":2},"55":{"body":47,"breadcrumbs":2,"title":0},"56":{"body":41,"breadcrumbs":2,"title":0},"57":{"body":2,"breadcrumbs":2,"title":0},"58":{"body":71,"breadcrumbs":5,"title":2},"59":{"body":325,"breadcrumbs":3,"title":0},"6":{"body":35,"breadcrumbs":3,"title":1},"60":{"body":217,"breadcrumbs":3,"title":0},"61":{"body":120,"breadcrumbs":4,"title":1},"62":{"body":375,"breadcrumbs":5,"title":2},"63":{"body":17,"breadcrumbs":5,"title":2},"64":{"body":78,"breadcrumbs":3,"title":0},"65":{"body":159,"breadcrumbs":4,"title":1},"66":{"body":220,"breadcrumbs":5,"title":1},"67":{"body":132,"breadcrumbs":3,"title":0},"68":{"body":198,"breadcrumbs":5,"title":2},"69":{"body":257,"breadcrumbs":4,"title":1},"7":{"body":51,"breadcrumbs":3,"title":1},"70":{"body":5,"breadcrumbs":3,"title":1},"71":{"body":238,"breadcrumbs":2,"title":0},"72":{"body":189,"breadcrumbs":2,"title":0},"8":{"body":84,"breadcrumbs":2,"title":0},"9":{"body":32,"breadcrumbs":2,"title":0}},"docs":{"0":{"body":"如果你是一位经验丰富的 C++ 程序员并且多少跟我差不多,那你在初次接触 C++11 的时候就会想,“是的,是的,我知道的。它还是 C++,就是多了点东西。”但随着你了解越多,你会为改变的幅度之巨感到震惊。auto 声明,基于 range 的 for 循环, lambda 表达式,还有右值引用完全改变了 C++ 的面貌,这还不说新的并发特性。然后还有一些惯常的改进。0 和 typedef 出局,nullptr 和别名声明新晋。枚举现在应该是限域的了。相比内置的指针类型,现在要更倾向去使用智能指针。移动对象通常也好过于拷贝它们。 有很多C++11的东西要学,先不提C++14了。 更重要的是,要学习怎样 高效地 使用新机能。如果你需要关于”现代“C++的特性的基础信息,学习资源有很多,但是你想找一些指南,教你怎样应用这些特性来写出正确、高效、可维护、可移植的程序,那就相当有挑战性了。这就是这本书的切入点。它不致力于介绍C++11和C++14的特性,而致力于它们的高效应用。 书中这些信息被打碎成不同指导方针,称为 条款 。想理解类型推导的不同形式?或者想知道什么时候该用(或者不该用)auto声明?你对为什么const成员函数应当线程安全,怎样使用std::unique_ptr实现Pimpl惯用法,为何要避免 lambda 表达式用默认捕获模式,或者std::atomic与volatile的区别感兴趣吗?答案都在这里。而且,答案无关于平台,顺应于标准。这本书是关于 可移植 C++的。 本书的条款是 指导方针 ,而不是 规则 ,因为指导方针也有例外。每个条款中最关键的部分不是提出的建议,而是建议背后的基本原理。一旦你阅读了它,你就明白你的程序的情况是否违反了条款的指导意见。本书的真正目的不是告诉你应该做什么不应该做什么,而是帮你深入理解C++11和C++14中各种东西是如何工作的。","breadcrumbs":"简介 » 简介","id":"0","title":"简介"},"1":{"body":"为了保证我们互相理解,对一些术语达成共识非常重要,首先有点讽刺的是,“C++”。有四个C++官方版本,每个版本名字后面带有相应ISO标准被采纳时的年份:C++98,C++03,C++11和C++14。C++98和C++03只有技术细节上的区别,所以本书统称为C++98。当我提到C++11时,我的意思是C++11和C++14,因为C++14是C++11的超集,当我写下C++14,我只意味着C++14。如果我仅仅提到C++,说明适用于所有的语言版本。 我使用的词 我意思中的语言版本 C++ 所有版本 C++98 C++98和C++03 C++11 C++11和C++14 C++14 C++14 因此,我可能会说C++重视效率(对所有版本正确),C++98缺少并发的支持(只对C++98和C++03正确),C++11支持 lambda 表达式(对C++11和C++14正确),C++14提供了普遍的函数返回类型推导(只对C++14正确)。 最遍布C++11各处的特性可能是移动语义了,移动语义的基础是区分右值和左值表达式。那是因为右值表明这个对象适合移动操作,而左值一般不适合。概念上(尽管不经常在实际上用),右值对应于从函数返回的临时对象,而左值对应于你可以引用的(can refer to)对象,或者通过名字,或者通过指针或左值引用。 对于判断一个表达式是否是左值的一个有用的启发就是,看看能否取得它的地址。如果能取地址,那么通常就是左值。如果不能,则通常是右值。这个启发的好处就是帮你记住,一个表达式的类型与它是左值还是右值无关。也就是说,有个类型T,你可以有类型T的左值和右值。当你碰到右值引用类型的形参时,记住这一点非常重要,因为形参本身是个左值: class Widget {\npublic: Widget(Widget&& rhs); //rhs是个左值, … //尽管它有个右值引用的类型\n}; 在这里,在Widget移动构造函数里取rhs的地址非常合理,所以rhs是左值,尽管它的类型是右值引用。(由于相似的原因,所有形参都是左值。) 那一小段代码揭示了我通常遵循的惯用法: 类的名字是Widget。每当我想指代任意的用户定义的类型时,我用Widget来代表。除非我需要展示类中的特定细节,否则我都直接使用Widget而不声明它。 我使用形参名rhs(“right-hand side”)。这是我喜欢的 移动操作 (即移动构造函数和移动赋值运算符)和 拷贝操作 (拷贝构造函数和拷贝赋值运算符)的形参名。我也在双目运算符的右侧形参用它: Matrix operator+(const Matrix& lhs, const Matrix& rhs); 我希望你并不奇怪,我用lhs表示“left-hand side”。 我在部分代码或者部分注释用特殊格式来吸引你的注意。(译者注:但是因为markdown没法在代码块中表明特殊格式,即原书使用的颜色改变和斜体注释,所以大部分情况下只能作罢,少部分地方会有额外说明。)在上面Widget移动构造函数中,我高亮了rhs的声明和“rhs是个左值”这部分注释。高亮代码不代表写的好坏。只是来提醒你需要额外的注意。 我使用“…”来表示“这里有一些别的代码”。这种窄省略号不同于C++11可变参数模板源代码中的宽省略号(“...”)。这听起来不太清楚,但实际并不。比如: template //这些是C++源代码的\nvoid processVals(const Ts&... params) //省略号\n{ … //这里意思是“这有一些别的代码”\n} processVals的声明表明在声明模板的类型形参时我使用typename,但这只是我的个人偏好;关键字class可以做同样的事情。在我展示从C++标准中摘录的代码的情况下,我使用class声明类型形参,因为那就是标准中的做法。 当使用另一个同类型的对象来初始化一个对象时,新的对象被称为是用来初始化的对象(译者注:initializing object,即源对象)的一个 副本 ( copy ),尽管这个副本是通过移动构造函数创建的。很抱歉地说,C++中没有术语来区别一个对象是拷贝构造的副本还是移动构造的副本(译者注:此处为了区别拷贝这个“动作”与拷贝得到的“东西”,将 copy 按语境译为拷贝(动作)和副本(东西),此处及接下来几段按此方式翻译。在后面的条款中可能会不加区别地全部翻译为“拷贝”。): void someFunc(Widget w); //someFunc的形参w是传值过来 Widget wid; //wid是个Widget someFunc(wid); //在这个someFunc调用中,w是通过拷贝构造函数 //创建的副本 someFunc(std::move(wid)); //在这个someFunc调用中,w是通过移动构造函数 //创建的副本 右值副本通常由移动构造产生,左值副本通常由拷贝构造产生。如果你仅仅知道一个对象是其他对象的副本,构造这个副本需要花费多大代价是没法说的。比如在上面的代码中,在不知道是用左值还是右值传给someFunc情况下,没法说来创建形参w花费代价有多大。(你必须还要知道移动和拷贝Widget的代价。) 在函数调用中,调用地传入的表达式称为函数的 实参 ( argument )。实参被用来初始化函数的 形参 ( parameter )。在上面第一次调用someFunc中,实参为wid。在第二次调用中,实参是std::move(wid)。两个调用中,形参都是w。实参和形参的区别非常重要,因为形参是左值,而用来初始化形参的实参可能是左值或者右值。这一点尤其与 完美转发 ( perfect forwarding )过程有关,被传给函数的实参以原实参的右值性( rvalueness )或左值性( lvalueness ),再被传给第二个函数。(完美转发讨论细节在 Item30 。) 设计优良的函数是 异常安全 ( exception safe )的,意味着他们至少提供基本的异常安全保证(即基本保证 basic guarantee )。这样的函数保证调用者在异常抛出时,程序不变量保持完整(即没有数据结构是毁坏的),且没有资源泄漏。有强异常安全保证的函数确保调用者在异常产生时,程序保持在调用前的状态。 当我提到“ 函数对象 ”时,我通常指的是某个支持operator()成员函数的类型的对象。换句话说,这个对象的行为像函数一样。偶尔我用稍微更普遍一些的术语,表示可以用非成员函数语法调用的任何东西(即“fuctionName(arguments)”)。这个广泛定义包括的不仅有支持operator()的对象,还有函数和类似C的函数指针。(较窄的定义来自于C++98,广泛点的定义来自于C++11。)将成员函数指针加进来的更深的普遍化产生了我们所知的 可调用对象 ( callable objects )。你通常可以忽略其中的微小区别,简单地认为函数对象和可调用对象为C++中可以用函数调用语法调用的东西。 通过 lambda 表达式创建的函数对象称为 闭包 ( closures )。没什么必要去区别 lambda 表达式和它们创建的闭包,所以我经常把它们统称 lambdas 。类似地,我几乎不区分 函数模板 ( function templates )(即产生函数的模板)和 模板函数 ( template functions )(即从函数模板产生的函数)。 类模板 ( class templates )和 模板类 ( template classes )同上。 C++中的许多东西都可被声明和定义。 声明 ( declarations )引入名字和类型,并不给出比如存放在哪或者怎样实现等的细节: extern int x; //对象声明 class Widget; //类声明 bool func(const Widget& w); //函数声明 enum class Color; //限域enum声明(见条款10) 定义 ( definitions )提供存储位置或者实现细节: int x; //对象定义 class Widget { //类定义 …\n}; bool func(const Widget& w)\n{ return w.size() < 10; } //函数定义 enum class Color\n{ Yellow, Red, Blue }; //限域enum定义 定义也有资格称为声明,所以我倾向于只有声明,除非这个东西有个定义非常重要。 我定义一个函数的 签名 ( signature )为它声明的一部分,这个声明指定了形参类型和返回类型。函数名和形参名不是签名的一部分。在上面的例子中,func的签名是bool(const Widget&)。函数声明中除了形参类型和返回类型之外的元素(比如noexcept或者constexpr,如果存在的话)都被排除在外。(noexcept和constexpr在 Item14 和 15 叙述。)“签名”的官方定义和我的有点不一样,但是对本书来说,我的定义更有用。(官方定义有时排除返回类型。) 新的C++标准保持了旧标准写的代码的有效性,但是偶尔标准化委员会 废弃 ( deprecate )一些特性。这些特性在标准化的“死囚区”中,可能在未来的标准中被移除。编译器可能警告也可能不警告这些废弃特性的使用,但是你应当尽量避免使用它们。它们不仅可能导致将来对移植的头痛,也通常不如来替代它们的新特性。例如,std::auto_ptr在C++11中被废弃,因为std::unique_ptr可以做同样的工作,而且只会做的更好。 有时标准说一个操作的结果有 未定义的行为 ( undefined behavior )。这意味着运行时表现是不可预测的,不用说你也想避开这种不确定性。有未定义行为的行动的例子是,在std::vector范围外使用方括号(“[]”),解引用未初始化的迭代器,或者引入数据竞争(即有两个或以上线程,至少一个是writer,同时访问相同的内存位置)。 我将那些比如从new返回的内置指针( build-in pointers )称为 原始指针 ( raw pointers )。原始指针的“反义词”是 智能指针 ( smart pointers )。智能指针通常重载指针解引用运算符(operator->和operator*),但在 Item20 中解释看std::weak_ptr是个例外。 在源代码注释中,我有时将“constructor”(构造函数)缩写为ctor,将“destructor”(析构函数)缩写为dtor。(译者注:但译文中基本都完整翻译了而没使用缩写。)","breadcrumbs":"简介 » 术语和惯例","id":"1","title":"术语和惯例"},"10":{"body":"Item 2: Understand auto type deduction 如果你已经读过 Item1 的模板类型推导,那么你几乎已经知道了auto类型推导的大部分内容,至于为什么不是全部是因为这里有一个auto不同于模板类型推导的例外。但这怎么可能?模板类型推导包括模板,函数,形参,但auto不处理这些东西啊。 你是对的,但没关系。auto类型推导和模板类型推导有一个直接的映射关系。它们之间可以通过一个非常规范非常系统化的转换流程来转换彼此。 在 Item1 中,模板类型推导使用下面这个函数模板 template\nvoid f(ParmaType param); 和这个调用来解释: f(expr); //使用一些表达式调用f 在f的调用中,编译器使用expr推导T和ParamType的类型。 当一个变量使用auto进行声明时,auto扮演了模板中T的角色,变量的类型说明符扮演了ParamType的角色。废话少说,这里便是更直观的代码描述,考虑这个例子: auto x = 27; 这里x的类型说明符是auto自己,另一方面,在这个声明中: const auto cx = x; 类型说明符是const auto。另一个: const auto & rx=cx; 类型说明符是const auto&。在这里例子中要推导x,rx和cx的类型,编译器的行为看起来就像是认为这里每个声明都有一个模板,然后使用合适的初始化表达式进行调用: template //概念化的模板用来推导x的类型\nvoid func_for_x(T param); func_for_x(27); //概念化调用: //param的推导类型是x的类型 template //概念化的模板用来推导cx的类型\nvoid func_for_cx(const T param); func_for_cx(x); //概念化调用: //param的推导类型是cx的类型 template //概念化的模板用来推导rx的类型\nvoid func_for_rx(const T & param); func_for_rx(x); //概念化调用: //param的推导类型是rx的类型 正如我说的,auto类型推导除了一个例外(我们很快就会讨论),其他情况都和模板类型推导一样。 Item1 基于ParamType——在函数模板中param的类型说明符——的不同特征,把模板类型推导分成三个部分来讨论。在使用auto作为类型说明符的变量声明中,类型说明符代替了ParamType,因此Item1描述的三个情景稍作修改就能适用于auto: 情景一:类型说明符是一个指针或引用但不是通用引用 情景二:类型说明符一个通用引用 情景三:类型说明符既不是指针也不是引用 我们早已看过情景一和情景三的例子: auto x = 27; //情景三(x既不是指针也不是引用)\nconst auto cx = x; //情景三(cx也一样)\nconst auto & rx=cx; //情景一(rx是非通用引用) 情景二像你期待的一样运作: auto&& uref1 = x; //x是int左值, //所以uref1类型为int&\nauto&& uref2 = cx; //cx是const int左值, //所以uref2类型为const int&\nauto&& uref3 = 27; //27是int右值, //所以uref3类型为int&& Item1 讨论并总结了对于non-reference类型说明符,数组和函数名如何退化为指针。那些内容也同样适用于auto类型推导: const char name[] = //name的类型是const char[13] \"R. N. Briggs\"; auto arr1 = name; //arr1的类型是const char*\nauto& arr2 = name; //arr2的类型是const char (&)[13] void someFunc(int, double); //someFunc是一个函数, //类型为void(int, double) auto func1 = someFunc; //func1的类型是void (*)(int, double)\nauto& func2 = someFunc; //func2的类型是void (&)(int, double) 就像你看到的那样,auto类型推导和模板类型推导几乎一样的工作,它们就像一个硬币的两面。 讨论完相同点接下来就是不同点,前面我们已经说到auto类型推导和模板类型推导有一个例外使得它们的工作方式不同,接下来我们要讨论的就是那个例外。 我们从一个简单的例子开始,如果你想声明一个带有初始值27的int,C++98提供两种语法选择: int x1 = 27;\nint x2(27); C++11由于也添加了用于支持统一初始化( uniform initialization )的语法: int x3 = { 27 };\nint x4{ 27 }; 总之,这四种不同的语法只会产生一个相同的结果:变量类型为int值为27 但是 Item5 解释了使用auto说明符代替指定类型说明符的好处,所以我们应该很乐意把上面声明中的int替换为auto,我们会得到这样的代码: auto x1 = 27;\nauto x2(27);\nauto x3 = { 27 };\nauto x4{ 27 }; 这些声明都能通过编译,但是他们不像替换之前那样有相同的意义。前面两个语句确实声明了一个类型为int值为27的变量,但是后面两个声明了一个存储一个元素27的 std::initializer_list类型的变量。 auto x1 = 27; //类型是int,值是27\nauto x2(27); //同上\nauto x3 = { 27 }; //类型是std::initializer_list, //值是{ 27 }\nauto x4{ 27 }; //同上 这就造成了auto类型推导不同于模板类型推导的特殊情况。当用auto声明的变量使用花括号进行初始化,auto类型推导推出的类型则为std::initializer_list。如果这样的一个类型不能被成功推导(比如花括号里面包含的是不同类型的变量),编译器会拒绝这样的代码: auto x5 = { 1, 2, 3.0 }; //错误!无法推导std::initializer_list中的T 就像注释说的那样,在这种情况下类型推导将会失败,但是对我们来说认识到这里确实发生了两种类型推导是很重要的。一种是由于auto的使用:x5的类型不得不被推导。因为x5使用花括号的方式进行初始化,x5必须被推导为std::initializer_list。但是std::initializer_list是一个模板。std::initializer_list会被某种类型T实例化,所以这意味着T也会被推导。 推导落入了这里发生的第二种类型推导——模板类型推导的范围。在这个例子中推导之所以失败,是因为在花括号中的值并不是同一种类型。 对于花括号的处理是auto类型推导和模板类型推导唯一不同的地方。当使用auto声明的变量使用花括号的语法进行初始化的时候,会推导出std::initializer_list的实例化,但是对于模板类型推导这样就行不通: auto x = { 11, 23, 9 }; //x的类型是std::initializer_list template //带有与x的声明等价的\nvoid f(T param); //形参声明的模板 f({ 11, 23, 9 }); //错误!不能推导出T 然而如果在模板中指定T是std::initializer_list而留下未知T,模板类型推导就能正常工作: template\nvoid f(std::initializer_list initList); f({ 11, 23, 9 }); //T被推导为int,initList的类型为 //std::initializer_list 因此auto类型推导和模板类型推导的真正区别在于,auto类型推导假定花括号表示std::initializer_list而模板类型推导不会这样(确切的说是不知道怎么办)。 你可能想知道为什么auto类型推导和模板类型推导对于花括号有不同的处理方式。我也想知道。哎,我至今没找到一个令人信服的解释。但是规则就是规则,这意味着你必须记住如果你使用auto声明一个变量,并用花括号进行初始化,auto类型推导总会得出std::initializer_list的结果。如果你使用**uniform initialization(花括号的方式进行初始化)**用得很爽你就得记住这个例外以免犯错,在C++11编程中一个典型的错误就是偶然使用了std::initializer_list类型的变量,这个陷阱也导致了很多C++程序员抛弃花括号初始化,只有不得不使用的时候再做考虑。(在 Item7 讨论了必须使用时该怎么做) 对于C++11故事已经说完了。但是对于C++14故事还在继续,C++14允许auto用于函数返回值并会被推导(参见 Item3 ),而且C++14的 lambda 函数也允许在形参声明中使用auto。但是在这些情况下auto实际上使用 模板类型推导 的那一套规则在工作,而不是auto类型推导,所以说下面这样的代码不会通过编译: auto createInitList()\n{ return { 1, 2, 3 }; //错误!不能推导{ 1, 2, 3 }的类型\n} 同样在C++14的lambda函数中这样使用auto也不能通过编译: std::vector v;\n…\nauto resetV = [&v](const auto& newValue){ v = newValue; }; //C++14\n…\nresetV({ 1, 2, 3 }); //错误!不能推导{ 1, 2, 3 }的类型 请记住: auto类型推导通常和模板类型推导相同,但是auto类型推导假定花括号初始化代表std::initializer_list,而模板类型推导不这样做 在C++14中auto允许出现在函数返回值或者 lambda 函数形参中,但是它的工作机制是模板类型推导那一套方案,而不是auto类型推导","breadcrumbs":"第一章 类型推导 » Item 2:理解auto类型推导 » 条款二:理解auto类型推导","id":"10","title":"条款二:理解auto类型推导"},"11":{"body":"Item 3: Understand decltype decltype是一个奇怪的东西。给它一个名字或者表达式decltype就会告诉你这个名字或者表达式的类型。通常,它会精确的告诉你你想要的结果。但有时候它得出的结果也会让你挠头半天,最后只能求助网上问答或参考资料寻求启示。 我们将从一个简单的情况开始,没有任何令人惊讶的情况。相比模板类型推导和auto类型推导(参见 Item1 和 Item2 ),decltype只是简单的返回名字或者表达式的类型: const int i = 0; //decltype(i)是const int bool f(const Widget& w); //decltype(w)是const Widget& //decltype(f)是bool(const Widget&) struct Point{ int x,y; //decltype(Point::x)是int\n}; //decltype(Point::y)是int Widget w; //decltype(w)是Widget if (f(w))… //decltype(f(w))是bool template //std::vector的简化版本\nclass vector{\npublic: … T& operator[](std::size_t index); …\n}; vector v; //decltype(v)是vector\n…\nif (v[0] == 0)… //decltype(v[0])是int& 看见了吧?没有任何奇怪的东西。 在C++11中,decltype最主要的用途就是用于声明函数模板,而这个函数返回类型依赖于形参类型。举个例子,假定我们写一个函数,一个形参为容器,一个形参为索引值,这个函数支持使用方括号的方式(也就是使用“[]”)访问容器中指定索引值的数据,然后在返回索引操作的结果前执行认证用户操作。函数的返回类型应该和索引操作返回的类型相同。 对一个T类型的容器使用operator[] 通常会返回一个T&对象,比如std::deque就是这样。但是std::vector有一个例外,对于std::vector,operator[]不会返回bool&,它会返回一个全新的对象(译注:MSVC的STL实现中返回的是std::_Vb_reference>>对象)。关于这个问题的详细讨论请参见 Item6 ,这里重要的是我们可以看到对一个容器进行operator[]操作返回的类型取决于容器本身。 使用decltype使得我们很容易去实现它,这是我们写的第一个版本,使用decltype计算返回类型,这个模板需要改良,我们把这个推迟到后面: template //可以工作,\nauto authAndAccess(Container& c, Index i) //但是需要改良 ->decltype(c[i])\n{ authenticateUser(); return c[i];\n} 函数名称前面的auto不会做任何的类型推导工作。相反的,他只是暗示使用了C++11的 尾置返回类型 语法,即在函数形参列表后面使用一个”->“符号指出函数的返回类型,尾置返回类型的好处是我们可以在函数返回类型中使用函数形参相关的信息。在authAndAccess函数中,我们使用c和i指定返回类型。如果我们按照传统语法把函数返回类型放在函数名称之前,c和i就未被声明所以不能使用。 在这种声明中,authAndAccess函数返回operator[]应用到容器中返回的对象的类型,这也正是我们期望的结果。 C++11允许自动推导单一语句的 lambda 表达式的返回类型, C++14扩展到允许自动推导所有的 lambda 表达式和函数,甚至它们内含多条语句。对于authAndAccess来说这意味着在C++14标准下我们可以忽略尾置返回类型,只留下一个auto。使用这种声明形式,auto标示这里会发生类型推导。更准确的说,编译器将会从函数实现中推导出函数的返回类型。 template //C++14版本,\nauto authAndAccess(Container& c, Index i) //不那么正确\n{ authenticateUser(); return c[i]; //从c[i]中推导返回类型\n} Item2 解释了函数返回类型中使用auto,编译器实际上是使用的模板类型推导的那套规则。如果那样的话这里就会有一些问题。正如我们之前讨论的,operator[]对于大多数T类型的容器会返回一个T&,但是 Item1 解释了在模板类型推导期间,表达式的引用性(reference-ness)会被忽略。基于这样的规则,考虑它会对下面用户的代码有哪些影响: std::deque d;\n…\nauthAndAccess(d, 5) = 10; //认证用户,返回d[5], //然后把10赋值给它 //无法通过编译器! 在这里d[5]本该返回一个int&,但是模板类型推导会剥去引用的部分,因此产生了int返回类型。函数返回的那个int是一个右值,上面的代码尝试把10赋值给右值int,C++11禁止这样做,所以代码无法编译。 要想让authAndAccess像我们期待的那样工作,我们需要使用decltype类型推导来推导它的返回值,即指定authAndAccess应该返回一个和c[i]表达式类型一样的类型。C++期望在某些情况下当类型被暗示时需要使用decltype类型推导的规则,C++14通过使用decltype(auto)说明符使得这成为可能。我们第一次看见decltype(auto)可能觉得非常的矛盾(到底是decltype还是auto?),实际上我们可以这样解释它的意义:auto说明符表示这个类型将会被推导,decltype说明decltype的规则将会被用到这个推导过程中。因此我们可以这样写authAndAccess: template //C++14版本,\ndecltype(auto) //可以工作,\nauthAndAccess(Container& c, Index i) //但是还需要\n{ //改良 authenticateUser(); return c[i];\n} 现在authAndAccess将会真正的返回c[i]的类型。现在事情解决了,一般情况下c[i]返回T&,authAndAccess也会返回T&,特殊情况下c[i]返回一个对象,authAndAccess也会返回一个对象。 decltype(auto)的使用不仅仅局限于函数返回类型,当你想对初始化表达式使用decltype推导的规则,你也可以使用: Widget w; const Widget& cw = w; auto myWidget1 = cw; //auto类型推导 //myWidget1的类型为Widget\ndecltype(auto) myWidget2 = cw; //decltype类型推导 //myWidget2的类型是const Widget& 但是这里有两个问题困惑着你。一个是我之前提到的authAndAccess的改良至今都没有描述。让我们现在加上它。 再看看C++14版本的authAndAccess声明: template\ndecltype(auto) authAndAccess(Container& c, Index i); 容器通过传引用的方式传递非常量左值引用(lvalue-reference-to-non- const ),因为返回一个引用允许用户可以修改容器。但是这意味着在不能给这个函数传递右值容器,右值不能被绑定到左值引用上(除非这个左值引用是一个const(lvalue-references-to- const ),但是这里明显不是)。 公认的向authAndAccess传递一个右值是一个 edge case (译注:在极限操作情况下会发生的事情,类似于会发生但是概率较小的事情)。一个右值容器,是一个临时对象,通常会在authAndAccess调用结束被销毁,这意味着authAndAccess返回的引用将会成为一个悬置的(dangle)引用。但是使用向authAndAccess传递一个临时变量也并不是没有意义,有时候用户可能只是想简单的获得临时容器中的一个元素的拷贝,比如这样: std::deque makeStringDeque(); //工厂函数 //从makeStringDeque中获得第五个元素的拷贝并返回\nauto s = authAndAccess(makeStringDeque(), 5); 要想支持这样使用authAndAccess我们就得修改一下当前的声明使得它支持左值和右值。重载是一个不错的选择(一个函数重载声明为左值引用,另一个声明为右值引用),但是我们就不得不维护两个重载函数。另一个方法是使authAndAccess的引用可以绑定左值和右值, Item24 解释了那正是通用引用能做的,所以我们这里可以使用通用引用进行声明: template //现在c是通用引用\ndecltype(auto) authAndAccess(Container&& c, Index i); 在这个模板中,我们不知道我们操纵的容器的类型是什么,那意味着我们同样不知道它使用的索引对象(index objects)的类型,对一个未知类型的对象使用传值通常会造成不必要的拷贝,对程序的性能有极大的影响,还会造成对象切片行为(参见 item41 ),以及给同事落下笑柄。但是就容器索引来说,我们遵照标准模板库对于索引的处理是有理由的(比如std::string,std::vector和std::deque的operator[]),所以我们坚持传值调用。 然而,我们还需要更新一下模板的实现,让它能听从 Item25 的告诫应用std::forward实现通用引用: template //最终的C++14版本\ndecltype(auto)\nauthAndAccess(Container&& c, Index i)\n{ authenticateUser(); return std::forward(c)[i];\n} 这样就能对我们的期望交上一份满意的答卷,但是这要求编译器支持C++14。如果你没有这样的编译器,你还需要使用C++11版本的模板,它看起来和C++14版本的极为相似,除了你不得不指定函数返回类型之外: template //最终的C++11版本\nauto\nauthAndAccess(Container&& c, Index i)\n->decltype(std::forward(c)[i])\n{ authenticateUser(); return std::forward(c)[i];\n} 另一个问题是就像我在条款的开始唠叨的那样,decltype通常会产生你期望的结果,但并不总是这样。在 极少数情况下 它产生的结果可能让你很惊讶。老实说如果你不是一个大型库的实现者你不太可能会遇到这些异常情况。 为了 完全 理解decltype的行为,你需要熟悉一些特殊情况。它们大多数都太过晦涩以至于几乎没有书进行有过权威的讨论,这本书也不例外,但是其中的一个会让我们更加理解decltype的使用。 将decltype应用于变量名会产生该变量名的声明类型。虽然变量名都是左值表达式,但这不会影响decltype的行为。(译者注:这里是说对于单纯的变量名,decltype只会返回变量的声明类型)然而,对于比单纯的变量名更复杂的左值表达式,decltype可以确保报告的类型始终是左值引用。也就是说,如果一个不是单纯变量名的左值表达式的类型是T,那么decltype会把这个表达式的类型报告为T&。这几乎没有什么太大影响,因为大多数左值表达式的类型天生具备一个左值引用修饰符。例如,返回左值的函数总是返回左值引用。 这个行为暗含的意义值得我们注意,在: int x = 0; 中,x是一个变量的名字,所以decltype(x)是int。但是如果用一个小括号包覆这个名字,比如这样(x) ,就会产生一个比名字更复杂的表达式。对于名字来说,x是一个左值,C++11定义了表达式(x)也是一个左值。因此decltype((x))是int&。用小括号覆盖一个名字可以改变decltype对于名字产生的结果。 在C++11中这稍微有点奇怪,但是由于C++14允许了decltype(auto)的使用,这意味着你在函数返回语句中细微的改变就可以影响类型的推导: decltype(auto) f1()\n{ int x = 0; … return x; //decltype(x)是int,所以f1返回int\n} decltype(auto) f2()\n{ int x = 0; return (x); //decltype((x))是int&,所以f2返回int&\n} 注意不仅f2的返回类型不同于f1,而且它还引用了一个局部变量!这样的代码将会把你送上未定义行为的特快列车,一辆你绝对不想上第二次的车。 当使用decltype(auto)的时候一定要加倍的小心,在表达式中看起来无足轻重的细节将会影响到decltype(auto)的推导结果。为了确认类型推导是否产出了你想要的结果,请参见 Item4 描述的那些技术。 同时你也不应该忽略decltype这块大蛋糕。没错,decltype(单独使用或者与auto一起用)可能会偶尔产生一些令人惊讶的结果,但那毕竟是少数情况。通常,decltype都会产生你想要的结果,尤其是当你对一个变量使用decltype时,因为在这种情况下,decltype只是做一件本分之事:它产出变量的声明类型。 请记住: decltype总是不加修改的产生变量或者表达式的类型。 对于T类型的不是单纯的变量名的左值表达式,decltype总是产出T的引用即T&。 C++14支持decltype(auto),就像auto一样,推导出类型,但是它使用decltype的规则进行推导。","breadcrumbs":"第一章 类型推导 » Item 3:理解decltype » 条款三:理解decltype","id":"11","title":"条款三:理解decltype"},"12":{"body":"Item 4: Know how to view deduced types 选择使用工具查看类型推导,取决于软件开发过程中你想在哪个阶段显示类型推导信息。我们探究三种方案:在你编辑代码的时候获得类型推导的结果,在编译期间获得结果,在运行时获得结果。","breadcrumbs":"第一章 类型推导 » Item 4:学会查看类型推导结果 » 条款四:学会查看类型推导结果","id":"12","title":"条款四:学会查看类型推导结果"},"13":{"body":"在IDE中的代码编辑器通常可以显示程序代码中变量,函数,参数的类型,你只需要简单的把鼠标移到它们的上面,举个例子,有这样的代码中: const int theAnswer = 42; auto x = theAnswer;\nauto y = &theAnswer; IDE编辑器可以直接显示x推导的结果为int,y推导的结果为const int*。 为此,你的代码必须或多或少的处于可编译状态,因为IDE之所以能提供这些信息是因为一个C++编译器(或者至少是前端中的一个部分)运行于IDE中。如果这个编译器对你的代码不能做出有意义的分析或者推导,它就不会显示推导的结果。 对于像int这样简单的推导,IDE产生的信息通常令人很满意。正如我们将看到的,如果更复杂的类型出现时,IDE提供的信息就几乎没有什么用了。","breadcrumbs":"第一章 类型推导 » Item 4:学会查看类型推导结果 » IDE编辑器","id":"13","title":"IDE编辑器"},"14":{"body":"另一个获得推导结果的方法是使用编译器出错时提供的错误消息。这些错误消息无形的提到了造成我们编译错误的类型是什么。 举个例子,假如我们想看到之前那段代码中x和y的类型,我们可以首先声明一个类模板但 不定义 。就像这样: template //只对TD进行声明\nclass TD; //TD == \"Type Displayer\" 如果尝试实例化这个类模板就会引出一个错误消息,因为这里没有用来实例化的类模板定义。为了查看x和y的类型,只需要使用它们的类型去实例化TD: TD xType; //引出包含x和y\nTD yType; //的类型的错误消息 我使用 variableName Type 的结构来命名变量,因为这样它们产生的错误消息可以有助于我们查找。对于上面的代码,我的编译器产生了这样的错误信息,我取一部分贴到下面: error: aggregate 'TD xType' has incomplete type and cannot be defined\nerror: aggregate 'TD yType' has incomplete type and cannot be defined 另一个编译器也产生了一样的错误,只是格式稍微改变了一下: error: 'xType' uses undefined class 'TD'\nerror: 'yType' uses undefined class 'TD' 除了格式不同外,几乎所有我测试过的编译器都产生了这样有用的错误消息。","breadcrumbs":"第一章 类型推导 » Item 4:学会查看类型推导结果 » 编译器诊断","id":"14","title":"编译器诊断"},"15":{"body":"使用printf的方法使类型信息只有在运行时才会显示出来(尽管我不是非常建议你使用printf),但是它提供了一种格式化输出的方法。现在唯一的问题是只需对于你关心的变量使用一种优雅的文本表示。“这有什么难的,“你这样想,”这正是typeid和std::type_info::name的价值所在”。为了实现我们想要查看x和y的类型的需求,你可能会这样写: std::cout << typeid(x).name() << '\\n'; //显示x和y的类型\nstd::cout << typeid(y).name() << '\\n'; 这种方法对一个对象如x或y调用typeid产生一个std::type_info的对象,然后std::type_info里面的成员函数name()来产生一个C风格的字符串(即一个const char*)表示变量的名字。 调用std::type_info::name不保证返回任何有意义的东西,但是库的实现者尝试尽量使它们返回的结果有用。实现者们对于“有用”有不同的理解。举个例子,GNU和Clang环境下x的类型会显示为”i“,y会显示为”PKi“,这样的输出你必须要问问编译器实现者们才能知道他们的意义:”i“表示”int“,”PK“表示”pointer to konst const“(指向常量的指针)。(这些编译器都提供一个工具c++filt,解释这些“混乱的”类型)Microsoft的编译器输出得更直白一些:对于x输出”int“对于y输出”int const *“ 因为对于x和y来说这样的结果是正确的,你可能认为问题已经接近了,别急,考虑一个更复杂的例子: template //要调用的模板函数\nvoid f(const T& param); std::vector createVec(); //工厂函数 const auto vw = createVec(); //使用工厂函数返回值初始化vw if (!vw.empty()){ f(&vw[0]); //调用f …\n} 在这段代码中包含了一个用户定义的类型Widget,一个STL容器std::vector和一个auto变量vw,这个更现实的情况是你可能在会遇到的并且想获得他们类型推导的结果,比如模板类型形参T,比如函数f形参param。 从这里中我们不难看出typeid的问题所在。我们在f中添加一些代码来显示类型: template\nvoid f(const T& param)\n{ using std::cout; cout << \"T = \" << typeid(T).name() << '\\n'; //显示T cout << \"param = \" << typeid(param).name() << '\\n'; //显示 … //param\n} //的类型 GNU和Clang执行这段代码将会输出这样的结果 T = PK6Widget\nparam = PK6Widget 我们早就知道在这些编译器中PK表示“pointer to const”,所以只有数字6对我们来说是神奇的。其实数字是类名称(Widget)的字符串长度,所以这些编译器告诉我们T和param都是const Widget*。 Microsoft的编译器也同意上述言论: T = class Widget const *\nparam = class Widget const * 这三个独立的编译器产生了相同的信息并表示信息非常准确,当然看起来不是那么准确。在模板f中,param的声明类型是const T&。难道你们不觉得T和param类型相同很奇怪吗?比如T是int,param的类型应该是const int&而不是相同类型才对吧。 遗憾的是,事实就是这样,std::type_info::name的结果并不总是可信的,就像上面一样,三个编译器对param的报告都是错误的。因为它们本质上可以不正确,因为std::type_info::name规范批准像传值形参一样来对待这些类型。正如 Item1 提到的,如果传递的是一个引用,那么引用部分(reference-ness)将被忽略,如果忽略后还具有const或者volatile,那么常量性constness或者易变性volatileness也会被忽略。那就是为什么param的类型const Widget * const &会输出为const Widget *,首先引用被忽略,然后这个指针自身的常量性constness被忽略,剩下的就是指针指向一个常量对象。 同样遗憾的是,IDE编辑器显示的类型信息也不总是可靠的,或者说不总是有用的。还是一样的例子,一个IDE编辑器可能会把T的类型显示为(我没有胡编乱造): const\nstd::_Simple_types>::_Alloc>::value_type>::value_type * 同样把param的类型显示为 const std::_Simple_types<...>::value_type *const & 这个比起T来说要简单一些,但是如果你不知道“...”表示编译器忽略T的部分类型那么可能你还是会产生困惑。如果你运气好点你的IDE可能表现得比这个要好一些。 比起运气如果你更倾向于依赖库,那么你乐意被告知std::type_info::name和IDE不怎么好,Boost TypeIndex库(通常写作 Boost.TypeIndex )是更好的选择。这个库不是标准C++的一部分,也不是IDE或者TD这样的模板。Boost库(可在 boost.com 获得)是跨平台,开源,有良好的开源协议的库,这意味着使用Boost和STL一样具有高度可移植性。 这里是如何使用Boost.TypeIndex得到f的类型的代码 #include template\nvoid f(const T& param)\n{ using std::cout; using boost::typeindex::type_id_with_cvr; //显示T cout << \"T = \" << type_id_with_cvr().pretty_name() << '\\n'; //显示param类型 cout << \"param = \" << type_id_with_cvr().pretty_name() << '\\n';\n} boost::typeindex::type_id_with_cvr获取一个类型实参(我们想获得相应信息的那个类型),它不消除实参的const,volatile和引用修饰符(因此模板名中有“with_cvr”)。结果是一个boost::typeindex::type_index对象,它的pretty_name成员函数输出一个std::string,包含我们能看懂的类型表示。 基于这个f的实现版本,再次考虑那个使用typeid时获取param类型信息出错的调用: std::vetor createVec(); //工厂函数\nconst auto vw = createVec(); //使用工厂函数返回值初始化vw\nif (!vw.empty()){ f(&vw[0]); //调用f …\n} 在GNU和Clang的编译器环境下,使用Boost.TypeIndex版本的f最后会产生下面的(准确的)输出: T = Widget const *\nparam = Widget const * const& 在Microsoft的编译器环境下,结果也是极其相似: T = class Widget const *\nparam = class Widget const * const & 这样近乎一致的结果是很不错的,但是请记住IDE,编译器错误诊断或者像Boost.TypeIndex这样的库只是用来帮助你理解编译器推导的类型是什么。它们是有用的,但是作为本章结束语我想说它们根本不能替代你对 Item1 - 3 提到的类型推导的理解。 请记住: 类型推断可以从IDE看出,从编译器报错看出,从Boost TypeIndex库的使用看出 这些工具可能既不准确也无帮助,所以理解C++类型推导规则才是最重要的","breadcrumbs":"第一章 类型推导 » Item 4:学会查看类型推导结果 » 运行时输出","id":"15","title":"运行时输出"},"16":{"body":"CHAPTER 2 auto 从概念上来说,auto要多简单有多简单,但是它比看起来要微妙一些。使用它可以存储类型,当然,它也会犯一些错误,而且比之手动声明一些复杂类型也会存在一些性能问题。此外,从程序员的角度来说,如果按照符合规定的流程走,那auto类型推导的一些结果是错误的。当这些情况发生时,对我们来说引导auto产生正确的结果是很重要的,因为严格按照说明书上面的类型写声明虽然可行但是最好避免。 本章简单的覆盖了auto的里里外外。","breadcrumbs":"第二章 auto » Item 5:优先考虑auto而非显式类型声明 » 第2章 auto","id":"16","title":"第2章 auto"},"17":{"body":"Item 5: Prefer auto to explicit type declarations 哈,开心一下: int x; 等等,该死!我忘记了初始化x,所以x的值是不确定的。它可能会被初始化为0,这得取决于工作环境。哎。 别介意,让我们转换一个话题, 对一个局部变量使用解引用迭代器的方式初始化: template //对从b到e的所有元素使用\nvoid dwim(It b, It e) //dwim(“do what I mean”)算法\n{ while (b != e) { typename std::iterator_traits::value_type currValue = *b; … }\n} 嘿!typename std::iterator_traits::value_type是想表达迭代器指向的元素的值的类型吗?我无论如何都说不出它是多么有趣这样的话,该死!等等,我早就说过了吗? 好吧,声明一个局部变量,类型是一个闭包,闭包的类型只有编译器知道,因此我们写不出来,该死! 该死该死该死,C++编程不应该是这样不愉快的体验。 别担心,它只在过去是这样,到了C++11所有的这些问题都消失了,这都多亏了auto。auto变量从初始化表达式中推导出类型,所以我们必须初始化。这意味着当你在现代化C++的高速公路上飞奔的同时你不得不对只声明不初始化变量的老旧方法说拜拜: int x1; //潜在的未初始化的变量 auto x2; //错误!必须要初始化 auto x3 = 0; //没问题,x已经定义了 而且即使使用解引用迭代器初始化局部变量也不会对你的高速驾驶有任何影响 template //如之前一样\nvoid dwim(It b,It e)\n{ while (b != e) { auto currValue = *b; … }\n} 因为使用 Item2 所述的auto类型推导技术,它甚至能表示一些只有编译器才知道的类型: auto derefUPLess = [](const std::unique_ptr &p1, //用于std::unique_ptr const std::unique_ptr &p2) //指向的Widget类型的 { return *p1 < *p2; }; //比较函数 很酷对吧,如果使用C++14,将会变得更酷,因为 lambda 表达式中的形参也可以使用auto: auto derefLess = //C++14版本 [](const auto& p1, //被任何像指针一样的东西 const auto& p2) //指向的值的比较函数 { return *p1 < *p2; }; 尽管这很酷,但是你可能会想我们完全不需要使用auto声明局部变量来保存一个闭包,因为我们可以使用std::function对象。没错,我们的确可以那么做,但是事情可能不是完全如你想的那样。当然现在你可能会问,std::function对象到底是什么。让我来给你解释一下。 std::function是一个C++11标准模板库中的一个模板,它泛化了函数指针的概念。与函数指针只能指向函数不同,std::function可以指向任何可调用对象,也就是那些像函数一样能进行调用的东西。当你声明函数指针时你必须指定函数类型(即函数签名),同样当你创建std::function对象时你也需要提供函数签名,由于它是一个模板所以你需要在它的模板参数里面提供。举个例子,假设你想声明一个std::function对象func使它指向一个可调用对象,比如一个具有这样函数签名的函数, bool(const std::unique_ptr &, //C++11 const std::unique_ptr &) //std::unique_ptr //比较函数的签名 你就得这么写: std::function &, const std::unique_ptr &)> func; 因为 lambda 表达式能产生一个可调用对象,所以我们现在可以把闭包存放到std::function对象中。这意味着我们可以不使用auto写出C++11版的derefUPLess: std::function &, const std::unique_ptr &)>\nderefUPLess = [](const std::unique_ptr &p1, const std::unique_ptr &p2) { return *p1 < *p2; }; 语法冗长不说,还需要重复写很多形参类型,使用std::function还不如使用auto。用auto声明的变量保存一个和闭包一样类型的(新)闭包,因此使用了与闭包相同大小存储空间。实例化std::function并声明一个对象这个对象将会有固定的大小。这个大小可能不足以存储一个闭包,这个时候std::function的构造函数将会在堆上面分配内存来存储,这就造成了使用std::function比auto声明变量会消耗更多的内存。并且通过具体实现我们得知通过std::function调用一个闭包几乎无疑比auto声明的对象调用要慢。换句话说,std::function方法比auto方法要更耗空间且更慢,还可能有 out-of-memory 异常。并且正如上面的例子,比起写std::function实例化的类型来,使用auto要方便得多。在这场存储闭包的比赛中,auto无疑取得了胜利(也可以使用std::bind来生成一个闭包,但在 Item34 我会尽我最大努力说服你使用 lambda 表达式代替std::bind) 使用auto除了可以避免未初始化的无效变量,省略冗长的声明类型,直接保存闭包外,它还有一个好处是可以避免一个问题,我称之为与类型快捷方式(type shortcuts)有关的问题。你将看到这样的代码——甚至你会这么写: std::vector v;\n…\nunsigned sz = v.size(); v.size()的标准返回类型是std::vector::size_type,但是只有少数开发者意识到这点。std::vector::size_type实际上被指定为无符号整型,所以很多人都认为用unsigned就足够了,写下了上述的代码。这会造成一些有趣的结果。举个例子,在 Windows 32-bit 上std::vector::size_type和unsigned是一样的大小,但是在 Windows 64-bit 上std::vector::size_type是64位,unsigned是32位。这意味着这段代码在Windows 32-bit上正常工作,但是当把应用程序移植到Windows 64-bit上时就可能会出现一些问题。谁愿意花时间处理这些细枝末节的问题呢? 所以使用auto可以确保你不需要浪费时间: auto sz =v.size(); //sz的类型是std::vector::size_type 你还是不相信使用auto是多么明智的选择?考虑下面的代码: std::unordered_map m;\n… for(const std::pair& p : m)\n{ … //用p做一些事\n} 看起来好像很合情合理的表达,但是这里有一个问题,你看到了吗? 要想看到错误你就得知道std::unordered_map的 key 是const的,所以 hash table (std::unordered_map本质上的东西)中的std::pair的类型不是std::pair,而是std::pair。但那不是在循环中的变量p声明的类型。编译器会努力的找到一种方法把std::pair(即 hash table 中的东西)转换为std::pair(p的声明类型)。它会成功的,因为它会通过拷贝m中的对象创建一个临时对象,这个临时对象的类型是p想绑定到的对象的类型,即m中元素的类型,然后把p的引用绑定到这个临时对象上。在每个循环迭代结束时,临时对象将会销毁,如果你写了这样的一个循环,你可能会对它的一些行为感到非常惊讶,因为你确信你只是让成为p指向m中各个元素的引用而已。 使用auto可以避免这些很难被意识到的类型不匹配的错误: for(const auto& p : m)\n{ … //如之前一样\n} 这样无疑更具效率,且更容易书写。而且,这个代码有一个非常吸引人的特性,如果你获取p的地址,你确实会得到一个指向m中元素的指针。在没有auto的版本中p会指向一个临时变量,这个临时变量在每次迭代完成时会被销毁。 前面这两个例子——应当写std::vector::size_type时写了unsigned,应当写std::pair时写了std::pair——说明了显式的指定类型可能会导致你不想看到的类型转换。如果你使用auto声明目标变量你就不必担心这个问题。 基于这些原因我建议你优先考虑auto而非显式类型声明。然而auto也不是完美的。每个auto变量都从初始化表达式中推导类型,有一些表达式的类型和我们期望的大相径庭。关于在哪些情况下会发生这些问题,以及你可以怎么解决这些问题我们在 Item2 和 6 讨论,所以这里我不再赘述。我想把注意力放到你可能关心的另一点:使用auto代替传统类型声明对源码可读性的影响。 首先,深呼吸,放松,auto是 可选项 ,不是 命令 ,在某些情况下如果你的专业判断告诉你使用显式类型声明比auto要更清晰更易维护,那你就不必再坚持使用auto。但是要牢记,C++没有在其他众所周知的语言所拥有的类型推导( type inference )上开辟新土地。其他静态类型的过程式语言(如C#、D、Sacla、Visual Basic)或多或少都有等价的特性,更不必提那些静态类型的函数式语言了(如ML、Haskell、OCaml、F#等)。在某种程度上,这是因为动态类型语言,如Perl、Python、Ruby等的成功;在这些语言中,几乎没有显式的类型声明。软件开发社区对于类型推导有丰富的经验,他们展示了在维护大型工业强度的代码上使用这种技术没有任何争议。 一些开发者也担心使用auto就不能瞥一眼源代码便知道对象的类型,然而,IDE扛起了部分担子(也考虑到了 Item4 中提到的IDE类型显示问题),在很多情况下,少量显示一个对象的类型对于知道对象的确切类型是有帮助的,这通常已经足够了。举个例子,要想知道一个对象是容器还是计数器还是智能指针,不需要知道它的确切类型。一个适当的变量名称就能告诉我们大量的抽象类型信息。 事实是显式指定类型通常只会引入一些微妙的错误,无论是在正确性还是效率方面。而且,如果初始化表达式的类型改变,则auto推导出的类型也会改变,这意味着使用auto可以帮助我们完成一些重构工作。举个例子,如果一个函数返回类型被声明为int,但是后来你认为将它声明为long会更好,调用它作为初始化表达式的变量会自动改变类型,但是如果你不使用auto你就不得不在源代码中挨个找到调用地点然后修改它们。 请记住: auto变量必须初始化,通常它可以避免一些移植性和效率性的问题,也使得重构更方便,还能让你少打几个字。 正如 Item2 和 6 讨论的,auto类型的变量可能会踩到一些陷阱。","breadcrumbs":"第二章 auto » Item 5:优先考虑auto而非显式类型声明 » 条款五:优先考虑auto而非显式类型声明","id":"17","title":"条款五:优先考虑auto而非显式类型声明"},"18":{"body":"Item 6: Use the explicitly typed initializer idiom when auto deduces undesired types 在 Item5 中解释了比起显式指定类型使用auto声明变量有若干技术优势,但是有时当你想向左转auto却向右转。举个例子,假如我有一个函数,参数为Widget,返回一个std::vector,这里的bool表示Widget是否提供一个独有的特性。 std::vector features(const Widget& w); 更进一步假设第5个 bit 表示Widget是否具有高优先级,我们可以写这样的代码: Widget w;\n…\nbool highPriority = features(w)[5]; //w高优先级吗?\n…\nprocessWidget(w, highPriority); //根据它的优先级处理w 这个代码没有任何问题。它会正常工作,但是如果我们使用auto代替highPriority的显式指定类型做一些看起来很无害的改变: auto highPriority = features(w)[5]; //w高优先级吗? 情况变了。所有代码仍然可编译,但是行为不再可预测: processWidget(w,highPriority); //未定义行为! 就像注释说的,这个processWidget是一个未定义行为。为什么呢?答案有可能让你很惊讶,使用auto后highPriority不再是bool类型。虽然从概念上来说std::vector意味着存放bool,但是std::vector的operator[]不会返回容器中元素的引用(这就是std::vector::operator[]可返回 除了bool以外 的任何类型),取而代之它返回一个std::vector::reference的对象(一个嵌套于std::vector中的类)。 std::vector::reference之所以存在是因为std::vector规定了使用一个打包形式(packed form)表示它的bool,每个bool占一个 bit 。那给std::vector的operator[]带来了问题,因为std::vector的operator[]应当返回一个T&,但是C++禁止对bits的引用。无法返回一个bool&,std::vector的operator[]返回一个 行为类似于 bool&的对象。要想成功扮演这个角色,bool&适用的上下文std::vector::reference也必须一样能适用。在std::vector::reference的特性中,使这个原则可行的特性是一个可以向bool的隐式转化。(不是bool&,是**bool**。要想完整的解释std::vector::reference能模拟bool&的行为所使用的一堆技术可能扯得太远了,所以这里简单地说隐式类型转换只是这个大型马赛克的一小块) 有了这些信息,我们再来看看原始代码的一部分: bool highPriority = features(w)[5]; //显式的声明highPriority的类型 这里,features返回一个std::vector对象后再调用operator[],operator[]将会返回一个std::vector::reference对象,然后再通过隐式转换赋值给bool变量highPriority。highPriority因此表示的是features返回的std::vector中的第五个 bit ,这也正如我们所期待的那样。 然后再对照一下当使用auto时发生了什么: auto highPriority = features(w)[5]; //推导highPriority的类型 同样的,features返回一个std::vector对象,再调用operator[],operator[]将会返回一个std::vector::reference对象,但是现在这里有一点变化了,auto推导highPriority的类型为std::vector::reference,但是highPriority对象没有第五 bit 的值。 这个值取决于std::vector::reference的具体实现。其中的一种实现是这样的(std::vector::reference)对象包含一个指向机器字( word )的指针,然后加上方括号中的偏移实现被引用 bit 这样的行为。然后再来考虑highPriority初始化表达的意思,注意这里假设std::vector::reference就是刚提到的实现方式。 调用features将返回一个std::vector临时对象,这个对象没有名字,为了方便我们的讨论,我这里叫他temp。operator[]在temp上调用,它返回的std::vector::reference包含一个指向存着这些 bit s的一个数据结构中的一个 word 的指针(temp管理这些 bit s),还有相应于第5个 bit 的偏移。highPriority是这个std::vector::reference的拷贝,所以highPriority也包含一个指针,指向temp中的这个 word ,加上相应于第5个 bit 的偏移。在这个语句结束的时候temp将会被销毁,因为它是一个临时变量。因此highPriority包含一个悬置的( dangling )指针,如果用于processWidget调用中将会造成未定义行为: processWidget(w, highPriority); //未定义行为! //highPriority包含一个悬置指针! std::vector::reference是一个代理类( proxy class )的例子:所谓代理类就是以模仿和增强一些类型的行为为目的而存在的类。很多情况下都会使用代理类,std::vector::reference展示了对std::vector使用operator[]来实现引用 bit 这样的行为。另外,C++标准模板库中的智能指针(见 第4章 )也是用代理类实现了对原始指针的资源管理行为。代理类的功能已被大家广泛接受。事实上,“Proxy”设计模式是软件设计这座万神庙中一直都存在的高级会员。 一些代理类被设计于用以对客户可见。比如std::shared_ptr和std::unique_ptr。其他的代理类则或多或少不可见,比如std::vector::reference就是不可见代理类的一个例子,还有它在std::bitset的胞弟std::bitset::reference。 在后者的阵营(注:指不可见代理类)里一些C++库也是用了表达式模板( expression templates )的黑科技。这些库通常被用于提高数值运算的效率。给出一个矩阵类Matrix和矩阵对象m1,m2,m3,m4,举个例子,这个表达式 Matrix sum = m1 + m2 + m3 + m4; 可以使计算更加高效,只需要使让operator+返回一个代理类代理结果而不是返回结果本身。也就是说,对两个Matrix对象使用operator+将会返回如Sum这样的代理类作为结果而不是直接返回一个Matrix对象。在std::vector::reference和bool中存在一个隐式转换,同样对于Matrix来说也可以存在一个隐式转换允许Matrix的代理类转换为Matrix,这让表达式等号“=”右边能产生代理对象来初始化sum。(这个对象应当编码整个初始化表达式,即类似于Sum, Matrix>, Matrix>的东西。客户应该避免看到这个实际的类型。) 作为一个通则,不可见的代理类通常不适用于auto。这样类型的对象的生命期通常不会设计为能活过一条语句,所以创建那样的对象你基本上就走向了违反程序库设计基本假设的道路。std::vector::reference就是这种情况,我们看到违反这个基本假设将导致未定义行为。 因此你想避开这种形式的代码: auto someVar = expression of \"invisible\" proxy class type; 但是你怎么能意识到你正在使用代理类?应用他们的软件不可能宣告它们的存在。它们被设计为 不可见 ,至少概念上说是这样!每当你发现它们,你真的应该舍弃 Item5 演示的auto所具有的诸多好处吗? 让我们首先回到如何找到它们的问题上。虽然“不可见”代理类都在程序员日常使用的雷达下方飞行,但是很多库都证明它们可以上方飞行。当你越熟悉你使用的库的基本设计理念,你的思维就会越活跃,不至于思维僵化认为代理类只能在这些库中使用。 当缺少文档的时候,可以去看看头文件。很少会出现源代码全都用代理对象,它们通常用于一些函数的返回类型,所以通常能从函数签名中看出它们的存在。这里有一份std::vector::operator[]的说明书: namespace std{ //来自于C++标准库 template class vector{ public: … class reference { … }; reference operator[](size_type n); … };\n} 假设你知道对std::vector使用operator[]通常会返回一个T&,在这里operator[]不寻常的返回类型提示你它使用了代理类。多关注你使用的接口可以暴露代理类的存在。 实际上, 很多开发者都是在跟踪一些令人困惑的复杂问题或在单元测试出错进行调试时才看到代理类的使用。不管你怎么发现它们的,一旦看到auto推导了代理类的类型而不是被代理的类型,解决方案并不需要抛弃auto。auto本身没什么问题,问题是auto不会推导出你想要的类型。解决方案是强制使用一个不同的类型推导形式,这种方法我通常称之为显式类型初始器惯用法( the explicitly typed initialized idiom )。 显式类型初始器惯用法使用auto声明一个变量,然后对表达式强制类型转换( cast )得出你期望的推导结果。举个例子,我们该怎么将这个惯用法施加到highPriority上? auto highPriority = static_cast(features(w)[5]); 这里,features(w)[5]还是返回一个std::vector::reference对象,就像之前那样,但是这个转型使得表达式类型为bool,然后auto才被用于推导highPriority。在运行时,对std::vector::operator[]返回的std::vector::reference执行它支持的向bool的转型,在这个过程中指向std::vector的指针已经被解引用。这就避开了我们之前的未定义行为。然后5将被用于指向 bit 的指针,bool值被用于初始化highPriority。 对于Matrix来说,显式类型初始器惯用法是这样的: auto sum = static_cast(m1 + m2 + m3 + m4); 应用这个惯用法不限制初始化表达式产生一个代理类。它也可以用于强调你声明了一个变量类型,它的类型不同于初始化表达式的类型。举个例子,假设你有这样一个表达式计算公差值: double calcEpsilon(); //返回公差值 calcEpsilon清楚的表明它返回一个double,但是假设你知道对于这个程序来说使用float的精度已经足够了,而且你很关心double和float的大小。你可以声明一个float变量储存calEpsilon的计算结果。 float ep = calcEpsilon(); //double到float隐式转换 但是这几乎没有表明“我确实要减少函数返回值的精度”。使用显式类型初始器惯用法我们可以这样: auto ep = static_cast(calcEpsilon()); 出于同样的原因,如果你故意想用整数类型存储一个表达式返回的浮点数类型的结果,你也可以使用这个方法。假如你需要计算一个随机访问迭代器(比如std::vector,std::deque或者std::array)中某元素的下标,你被提供一个0.0到1.0的double值表明这个元素离容器的头部有多远(0.5意味着位于容器中间)。进一步假设你很自信结果下标是int。如果容器是c,d是double类型变量,你可以用这样的方法计算容器下标: int index = d * c.size(); 但是这种写法并没有明确表明你想将右侧的double类型转换成int类型,显式类型初始器可以帮助你正确表意: auto index = static_cast(d * size()); 请记住: 不可见的代理类可能会使auto从表达式中推导出“错误的”类型 显式类型初始器惯用法强制auto推导出你想要的结果","breadcrumbs":"第二章 auto » Item 6:auto推导若非己愿,使用显式类型初始化惯用法 » 条款六:auto推导若非己愿,使用显式类型初始化惯用法","id":"18","title":"条款六:auto推导若非己愿,使用显式类型初始化惯用法"},"19":{"body":"CHAPTER 3 Moving to Modern C++ 说起知名的特性,C++11/14有一大堆可以吹的东西,auto,智能指针( smart pointer ),移动语义( move semantics ), lambda ,并发( concurrency )——每个都是如此的重要,这章将覆盖这些内容。掌握这些特性是必要的,要想成为高效率的现代C++程序员需要小步迈进。在从C++98小步迈进到现代C++过程中遇到的每个问题,本章都会一一回答。你什么时候应该用{}而不是()创建对象?为什么别名( alias )声明比typedef好?constexpr和const有什么不同?常量(const)成员函数和线程安全有什么关系?这个列表越列越多,这章将会逐个回答这些问题。","breadcrumbs":"第三章 移步现代C++ » Item 7:区别使用()和{}创建对象 » 第3章 移步现代C++","id":"19","title":"第3章 移步现代C++"},"2":{"body":"我尽力将本书写的清晰、准确、富含有用的信息,但是当然还有些去做得更好的办法。如果你找到了任何类型的错误(技术上的,叙述上的,语法上的,印刷上的等),或者有些建议如何改进本书,请给我发电子邮件到emc++@aristeia.com。新的印刷给了我改进《Modern Effective C++》的机会,但我也不能解决我不知道的问题! 要查看我所知道的事情,参见本书勘误表页,http://www.aristeia.com/BookErrata/emc++-errata.html 。","breadcrumbs":"简介 » 报告bug,提出改进意见","id":"2","title":"报告bug,提出改进意见"},"20":{"body":"Item 7: Distinguish between () and {} when creating objects 取决于你看问题的角度,C++11对象初始化的语法可能会让你觉得丰富的让人难以选择,亦或是乱的一塌糊涂。一般来说,初始化值要用圆括号()或者花括号{}括起来,或者放到等号\"=\"的右边: int x(0); //使用圆括号初始化 int y = 0; //使用\"=\"初始化 int z{ 0 }; //使用花括号初始化 在很多情况下,你可以使用\"=\"和花括号的组合: int z = { 0 }; //使用\"=\"和花括号 在这个条款的剩下部分,我通常会忽略\"=\"和花括号组合初始化的语法,因为C++通常把它视作和只有花括号一样。 “乱的一塌糊涂”是指在初始化中使用\"=\"可能会误导C++新手,使他们以为这里发生了赋值运算,然而实际并没有。对于像int这样的内置类型,研究两者区别就像在做学术,但是对于用户定义的类型而言,区别赋值运算符和初始化就非常重要了,因为它们涉及不同的函数调用: Widget w1; //调用默认构造函数 Widget w2 = w1; //不是赋值运算,调用拷贝构造函数 w1 = w2; //是赋值运算,调用拷贝赋值运算符(copy operator=) 甚至对于一些初始化语法,在一些情况下C++98没有办法表达预期的初始化行为。举个例子,要想直接创建并初始化一个存放一些特殊值的STL容器是不可能的(比如1,3,5)。 C++11使用统一初始化( uniform initialization )来整合这些混乱且不适于所有情景的初始化语法,所谓统一初始化是指在任何涉及初始化的地方都使用单一的初始化语法。 它基于花括号,出于这个原因我更喜欢称之为括号初始化。( 译注:注意,这里的括号初始化指的是花括号初始化,在没有歧义的情况下下文的括号初始化指的都是用花括号进行初始化;当与圆括号初始化同时存在并可能产生歧义时我会直接指出。 )统一初始化是一个概念上的东西,而括号初始化是一个具体语法结构。 括号初始化让你可以表达以前表达不出的东西。使用花括号,创建并指定一个容器的初始元素变得很容易: std::vector v{ 1, 3, 5 }; //v初始内容为1,3,5 括号初始化也能被用于为非静态数据成员指定默认初始值。C++11允许\"=\"初始化不加花括号也拥有这种能力: class Widget{ … private: int x{ 0 }; //没问题,x初始值为0 int y = 0; //也可以 int z(0); //错误!\n} 另一方面,不可拷贝的对象(例如std::atomic——见 Item40 )可以使用花括号初始化或者圆括号初始化,但是不能使用\"=\"初始化: std::atomic ai1{ 0 }; //没问题\nstd::atomic ai2(0); //没问题\nstd::atomic ai3 = 0; //错误! 因此我们很容易理解为什么括号初始化又叫统一初始化,在C++中这三种方式都被看做是初始化表达式,但是只有花括号任何地方都能被使用。 括号表达式还有一个少见的特性,即它不允许内置类型间隐式的变窄转换( narrowing conversion )。如果一个使用了括号初始化的表达式的值,不能保证由被初始化的对象的类型来表示,代码就不会通过编译: double x, y, z; int sum1{ x + y + z }; //错误!double的和可能不能表示为int 使用圆括号和\"=\"的初始化不检查是否转换为变窄转换,因为由于历史遗留问题它们必须要兼容老旧代码: int sum2(x + y +z); //可以(表达式的值被截为int) int sum3 = x + y + z; //同上 另一个值得注意的特性是括号表达式对于C++最令人头疼的解析问题有天生的免疫性。(译注:所谓最令人头疼的解析即 most vexing parse ,更多信息请参见 https://en.wikipedia.org/wiki/Most_vexing_parse 。)C++规定任何 可以被解析 为一个声明的东西 必须被解析 为声明。这个规则的副作用是让很多程序员备受折磨:他们可能想创建一个使用默认构造函数构造的对象,却不小心变成了函数声明。问题的根源是如果你调用带参构造函数,你可以这样做: Widget w1(10); //使用实参10调用Widget的一个构造函数 但是如果你尝试使用相似的语法调用Widget无参构造函数,它就会变成函数声明: Widget w2(); //最令人头疼的解析!声明一个函数w2,返回Widget 由于函数声明中形参列表不能带花括号,所以使用花括号初始化表明你想调用默认构造函数构造对象就没有问题: Widget w3{}; //调用没有参数的构造函数构造对象 关于括号初始化还有很多要说的。它的语法能用于各种不同的上下文,它防止了隐式的变窄转换,而且对于C++最令人头疼的解析也天生免疫。既然好到这个程度那为什么这个条款不叫“优先考虑括号初始化语法”呢? 括号初始化的缺点是有时它有一些令人惊讶的行为。这些行为使得括号初始化、std::initializer_list和构造函数参与重载决议时本来就不清不楚的暧昧关系进一步混乱。把它们放到一起会让看起来应该左转的代码右转。举个例子, Item2 解释了当auto声明的变量使用花括号初始化,变量类型会被推导为std::initializer_list,但是使用相同内容的其他初始化方式会产生更符合直觉的结果。所以,你越喜欢用auto,你就越不能用括号初始化。 在构造函数调用中,只要不包含std::initializer_list形参,那么花括号初始化和圆括号初始化都会产生一样的结果: class Widget { public: Widget(int i, bool b); //构造函数未声明 Widget(int i, double d); //std::initializer_list这个形参 …\n};\nWidget w1(10, true); //调用第一个构造函数\nWidget w2{10, true}; //也调用第一个构造函数\nWidget w3(10, 5.0); //调用第二个构造函数\nWidget w4{10, 5.0}; //也调用第二个构造函数 然而,如果有一个或者多个构造函数的声明包含一个std::initializer_list形参,那么使用括号初始化语法的调用更倾向于选择带std::initializer_list的那个构造函数。如果编译器遇到一个括号初始化并且有一个带std::initializer_list的构造函数,那么它一定会选择该构造函数。如果上面的Widget类有一个std::initializer_list作为参数的构造函数,就像这样: class Widget { public: Widget(int i, bool b); //同上 Widget(int i, double d); //同上 Widget(std::initializer_list il); //新添加的 …\n}; w2和w4将会使用新添加的构造函数,即使另一个非std::initializer_list构造函数和实参更匹配: Widget w1(10, true); //使用圆括号初始化,同之前一样 //调用第一个构造函数 Widget w2{10, true}; //使用花括号初始化,但是现在 //调用带std::initializer_list的构造函数 //(10 和 true 转化为long double) Widget w3(10, 5.0); //使用圆括号初始化,同之前一样 //调用第二个构造函数 Widget w4{10, 5.0}; //使用花括号初始化,但是现在 //调用带std::initializer_list的构造函数 //(10 和 5.0 转化为long double) 甚至普通构造函数和移动构造函数都会被带std::initializer_list的构造函数劫持: class Widget { public: Widget(int i, bool b); //同之前一样 Widget(int i, double d); //同之前一样 Widget(std::initializer_list il); //同之前一样 operator float() const; //转换为float …\n}; Widget w5(w4); //使用圆括号,调用拷贝构造函数 Widget w6{w4}; //使用花括号,调用std::initializer_list构造 //函数(w4转换为float,float转换为double) Widget w7(std::move(w4)); //使用圆括号,调用移动构造函数 Widget w8{std::move(w4)}; //使用花括号,调用std::initializer_list构造 //函数(与w6相同原因) 编译器一遇到括号初始化就选择带std::initializer_list的构造函数的决心是如此强烈,以至于就算带std::initializer_list的构造函数不能被调用,它也会硬选。 class Widget { public: Widget(int i, bool b); //同之前一样 Widget(int i, double d); //同之前一样 Widget(std::initializer_list il); //现在元素类型为bool … //没有隐式转换函数\n}; Widget w{10, 5.0}; //错误!要求变窄转换 这里,编译器会直接忽略前面两个构造函数(其中第二个构造函数是所有实参类型的最佳匹配),然后尝试调用std::initializer_list构造函数。调用这个函数将会把int(10)和double(5.0)转换为bool,由于会产生变窄转换(bool不能准确表示其中任何一个值),括号初始化拒绝变窄转换,所以这个调用无效,代码无法通过编译。 只有当没办法把括号初始化中实参的类型转化为std::initializer_list时,编译器才会回到正常的函数决议流程中。比如我们在构造函数中用std::initializer_list代替std::initializer_list,这时非std::initializer_list构造函数将再次成为函数决议的候选者,因为没有办法把int和bool转换为std::string: class Widget { public: Widget(int i, bool b); //同之前一样 Widget(int i, double d); //同之前一样 //现在std::initializer_list元素类型为std::string Widget(std::initializer_list il); … //没有隐式转换函数\n}; Widget w1(10, true); // 使用圆括号初始化,调用第一个构造函数\nWidget w2{10, true}; // 使用花括号初始化,现在调用第一个构造函数\nWidget w3(10, 5.0); // 使用圆括号初始化,调用第二个构造函数\nWidget w4{10, 5.0}; // 使用花括号初始化,现在调用第二个构造函数 代码的行为和我们刚刚的论述如出一辙。这里还有一个有趣的 边缘情况 。假如你使用的花括号初始化是空集,并且你欲构建的对象有默认构造函数,也有std::initializer_list构造函数。你的空的花括号意味着什么?如果它们意味着没有实参,就该使用默认构造函数,但如果它意味着一个空的std::initializer_list,就该调用std::initializer_list构造函数。 最终会调用默认构造函数。空的花括号意味着没有实参,不是一个空的std::initializer_list: class Widget { public: Widget(); //默认构造函数 Widget(std::initializer_list il); //std::initializer_list构造函数 … //没有隐式转换函数\n}; Widget w1; //调用默认构造函数\nWidget w2{}; //也调用默认构造函数\nWidget w3(); //最令人头疼的解析!声明一个函数 如果你 想 用空std::initializer来调用std::initializer_list构造函数,你就得创建一个空花括号作为函数实参——把空花括号放在圆括号或者另一个花括号内来界定你想传递的东西。 Widget w4({}); //使用空花括号列表调用std::initializer_list构造函数\nWidget w5{{}}; //同上 此时,括号初始化,std::initializer_list和构造函数重载的晦涩规则就会一下子涌进你的脑袋,你可能会想研究半天这些东西在你的日常编程中到底占多大比例。可能比你想象的要有用。因为std::vector作为受众之一会直接受到影响。std::vector有一个非std::initializer_list构造函数允许你去指定容器的初始大小,以及使用一个值填满你的容器。但它也有一个std::initializer_list构造函数允许你使用花括号里面的值初始化容器。如果你创建一个数值类型的std::vector(比如std::vector),然后你传递两个实参,把这两个实参放到圆括号和放到花括号中有天壤之别: std::vector v1(10, 20); //使用非std::initializer_list构造函数 //创建一个包含10个元素的std::vector, //所有的元素的值都是20\nstd::vector v2{10, 20}; //使用std::initializer_list构造函数 //创建包含两个元素的std::vector, //元素的值为10和20 让我们回到之前的话题。从以上讨论中我们得出两个重要结论。第一,作为一个类库作者,你需要意识到如果一堆重载的构造函数中有一个或者多个含有std::initializer_list形参,用户代码如果使用了括号初始化,可能只会看到你std::initializer_list版本的重载的构造函数。因此,你最好把你的构造函数设计为不管用户是使用圆括号还是使用花括号进行初始化都不会有什么影响。换句话说,了解了std::vector设计缺点后,你以后设计类的时候应该避免诸如此类的问题。 这里的暗语是如果一个类没有std::initializer_list构造函数,然后你添加一个,用户代码中如果使用括号初始化,可能会发现过去被决议为非std::initializer_list构造函数而现在被决议为新的函数。当然,这种事情也可能发生在你添加一个函数到那堆重载函数的时候:过去被决议为旧的重载函数而现在调用了新的函数。std::initializer_list重载不会和其他重载函数比较,它直接盖过了其它重载函数,其它重载函数几乎不会被考虑。所以如果你要加入std::initializer_list构造函数,请三思而后行。 第二,作为一个类库使用者,你必须认真的在花括号和圆括号之间选择一个来创建对象。大多数开发者都使用其中一种作为默认情况,只有当他们不能使用这种的时候才会考虑另一种。默认使用花括号初始化的开发者主要被适用面广、禁止变窄转换、免疫C++最令人头疼的解析这些优点所吸引。这些开发者知道在一些情况下(比如给定一个容器大小和一个初始值创建std::vector)要使用圆括号。默认使用圆括号初始化的开发者主要被C++98语法一致性、避免std::initializer_list自动类型推导、避免不会不经意间调用std::initializer_list构造函数这些优点所吸引。这些开发者也承认有时候只能使用花括号(比如创建一个包含着特定值的容器)。关于花括号初始化和圆括号初始化哪种更好大家没有达成一致,所以我的建议是选择一种并坚持使用它。 如果你是一个模板的作者,花括号和圆括号创建对象就更麻烦了。通常不能知晓哪个会被使用。举个例子,假如你想创建一个接受任意数量的参数来创建的对象。使用可变参数模板( variadic template )可以非常简单的解决: template //要使用的实参的类型\nvoid doSomeWork(Ts&&... params)\n{ create local T object from params... …\n} 在现实中我们有两种方式实现这个伪代码(关于std::forward请参见 Item25 ): T localObject(std::forward(params)...); //使用圆括号\nT localObject{std::forward(params)...}; //使用花括号 考虑这样的调用代码: std::vector v; …\ndoSomeWork>(10, 20); 如果doSomeWork创建localObject时使用的是圆括号,std::vector就会包含10个元素。如果doSomeWork创建localObject时使用的是花括号,std::vector就会包含2个元素。哪个是正确的?doSomeWork的作者不知道,只有调用者知道。 这正是标准库函数std::make_unique和std::make_shared(参见 Item21 )面对的问题。它们的解决方案是使用圆括号,并被记录在文档中作为接口的一部分。(注:更灵活的设计——允许调用者决定从模板来的函数应该使用圆括号还是花括号——是有可能的。详情参见 Andrzej’s C++ blog 在2013年6月5日的文章,“ Intuitive interface — Part I. ”) 请记住: 花括号初始化是最广泛使用的初始化语法,它防止变窄转换,并且对于C++最令人头疼的解析有天生的免疫性 在构造函数重载决议中,编译器会尽最大努力将括号初始化与std::initializer_list参数匹配,即便其他构造函数看起来是更好的选择 对于数值类型的std::vector来说使用花括号初始化和圆括号初始化会造成巨大的不同 在模板类选择使用圆括号初始化或使用花括号初始化创建对象是一个挑战。","breadcrumbs":"第三章 移步现代C++ » Item 7:区别使用()和{}创建对象 » 条款七:区别使用()和{}创建对象","id":"20","title":"条款七:区别使用()和{}创建对象"},"21":{"body":"Item 8: Prefer nullptr to 0 and NULL 你看这样对不对:字面值0是一个int不是指针。如果C++发现在当前上下文只能使用指针,它会很不情愿的把0解释为指针,但是那是最后的退路。一般来说C++的解析策略是把0看做int而不是指针。 实际上,NULL也是这样的。但在NULL的实现细节有些不确定因素,因为实现被允许给NULL一个除了int之外的整型类型(比如long)。这不常见,但也算不上问题所在。这里的问题不是NULL没有一个确定的类型,而是0和NULL都不是指针类型。 在C++98中,对指针类型和整型进行重载意味着可能导致奇怪的事情。如果给下面的重载函数传递0或NULL,它们绝不会调用指针版本的重载函数: void f(int); //三个f的重载函数\nvoid f(bool);\nvoid f(void*); f(0); //调用f(int)而不是f(void*) f(NULL); //可能不会被编译,一般来说调用f(int), //绝对不会调用f(void*) 而f(NULL)的不确定行为是由NULL的实现不同造成的。如果NULL被定义为0L(指的是0为long类型),这个调用就具有二义性,因为从long到int的转换或从long到bool的转换或0L到void*的转换都同样好。有趣的是源代码 表现出 的意思(“我使用空指针NULL调用f”)和 实际表达出 的意思(“我是用整型数据而不是空指针调用f”)是相矛盾的。这种违反直觉的行为导致C++98程序员都将避开同时重载指针和整型作为编程准则(译注:请务必注意结合上下文使用这条规则)。在C++11中这个编程准则也有效,因为尽管我这个条款建议使用nullptr,可能很多程序员还是会继续使用0或NULL,哪怕nullptr是更好的选择。 nullptr的优点是它不是整型。老实说它也不是一个指针类型,但是你可以把它认为是 所有 类型的指针。nullptr的真正类型是std::nullptr_t,在一个完美的循环定义以后,std::nullptr_t又被定义为nullptr。std::nullptr_t可以隐式转换为指向任何内置类型的指针,这也是为什么nullptr表现得像所有类型的指针。 使用nullptr调用f将会调用void*版本的重载函数,因为nullptr不能被视作任何整型: f(nullptr); //调用重载函数f的f(void*)版本 使用nullptr代替0和NULL可以避开了那些令人奇怪的函数重载决议,这不是它的唯一优势。它也可以使代码表意明确,尤其是当涉及到与auto声明的变量一起使用时。举个例子,假如你在一个代码库中遇到了这样的代码: auto result = findRecord( /* arguments */ );\nif (result == 0) { …\n} 如果你不知道findRecord返回了什么(或者不能轻易的找出),那么你就不太清楚到底result是一个指针类型还是一个整型。毕竟,0(用来测试result的值的那个)也可以像我们之前讨论的那样被解析。但是换一种假设如果你看到这样的代码: auto result = findRecord( /* arguments */ ); if (result == nullptr) { …\n} 这就没有任何歧义:result的结果一定是指针类型。 当模板出现时nullptr就更有用了。假如你有一些函数只能被合适的已锁互斥量调用。每个函数都有一个不同类型的指针: int f1(std::shared_ptr spw); //只能被合适的\ndouble f2(std::unique_ptr upw); //已锁互斥量\nbool f3(Widget* pw); //调用 如果这样传递空指针: std::mutex f1m, f2m, f3m; //用于f1,f2,f3函数的互斥量 using MuxGuard = //C++11的typedef,参见Item9 std::lock_guard;\n… { MuxGuard g(f1m); //为f1m上锁 auto result = f1(0); //向f1传递0作为空指针\n} //解锁 …\n{ MuxGuard g(f2m); //为f2m上锁 auto result = f2(NULL); //向f2传递NULL作为空指针\n} //解锁 …\n{ MuxGuard g(f3m); //为f3m上锁 auto result = f3(nullptr); //向f3传递nullptr作为空指针\n} //解锁 令人遗憾前两个调用没有使用nullptr,但是代码可以正常运行,这也许对一些东西有用。但是重复的调用代码——为互斥量上锁,调用函数,解锁互斥量——更令人遗憾。它让人很烦。模板就是被设计于减少重复代码,所以让我们模板化这个调用流程: template\nauto lockAndCall(FuncType func, MuxType& mutex, PtrType ptr) -> decltype(func(ptr))\n{ MuxGuard g(mutex); return func(ptr); } 如果你对函数返回类型(auto ... -> decltype(func(ptr)))感到困惑不解, Item3 可以帮助你。在C++14中代码的返回类型还可以被简化为decltype(auto): template\ndecltype(auto) lockAndCall(FuncType func, //C++14 MuxType& mutex, PtrType ptr)\n{ MuxGuard g(mutex); return func(ptr); } 可以写这样的代码调用lockAndCall模板(两个版本都可): auto result1 = lockAndCall(f1, f1m, 0); //错误!\n...\nauto result2 = lockAndCall(f2, f2m, NULL); //错误!\n...\nauto result3 = lockAndCall(f3, f3m, nullptr); //没问题 代码虽然可以这样写,但是就像注释中说的,前两个情况不能通过编译。在第一个调用中存在的问题是当0被传递给lockAndCall模板,模板类型推导会尝试去推导实参类型,0的类型总是int,所以这就是这次调用lockAndCall实例化出的ptr的类型。不幸的是,这意味着lockAndCall中func会被int类型的实参调用,这与f1期待的std::shared_ptr形参不符。传递0给lockAndCall本来想表示空指针,但是实际上得到的一个普通的int。把int类型看做std::shared_ptr类型给f1自然是一个类型错误。在模板lockAndCall中使用0之所以失败是因为在模板中,传给的是int但实际上函数期待的是一个std::shared_ptr。 第二个使用NULL调用的分析也是一样的。当NULL被传递给lockAndCall,形参ptr被推导为整型(译注:由于依赖于具体实现所以不一定是整数类型,所以用整型泛指int,long等类型),然后当ptr——一个int或者类似int的类型——传递给f2的时候就会出现类型错误,f2期待的是std::unique_ptr。 然而,使用nullptr是调用没什么问题。当nullptr传给lockAndCall时,ptr被推导为std::nullptr_t。当ptr被传递给f3的时候,隐式转换使std::nullptr_t转换为Widget*,因为std::nullptr_t可以隐式转换为任何指针类型。 模板类型推导将0和NULL推导为一个错误的类型(即它们的实际类型,而不是作为空指针的隐含意义),这就导致在当你想要一个空指针时,它们的替代品nullptr很吸引人。使用nullptr,模板不会有什么特殊的转换。另外,使用nullptr不会让你受到同重载决议特殊对待0和NULL一样的待遇。当你想用一个空指针,使用nullptr,不用0或者NULL。 记住 优先考虑nullptr而非0和NULL 避免重载指针和整型","breadcrumbs":"第三章 移步现代C++ » Item 8:优先考虑nullptr而非0和NULL » 条款八:优先考虑nullptr而非0和NULL","id":"21","title":"条款八:优先考虑nullptr而非0和NULL"},"22":{"body":"Item 9: Prefer alias declarations to typedefs 我相信每个人都同意使用STL容器是个好主意,并且我希望 Item18 能说服你让你觉得使用std:unique_ptr也是个好主意,但我猜没有人喜欢写上几次 std::unique_ptr>这样的类型,它可能会让你患上腕管综合征的风险大大增加。 避免上述医疗悲剧也很简单,引入typedef即可: typedef std::unique_ptr> UPtrMapSS; 但typedef是C++98的东西。虽然它可以在C++11中工作,但是C++11也提供了一个别名声明( alias declaration ): using UPtrMapSS = std::unique_ptr>; 由于这里给出的typedef和别名声明做的都是完全一样的事情,我们有理由想知道会不会出于一些技术上的原因两者有一个更好。 这里,在说它们之前我想提醒一下很多人都发现当声明一个函数指针时别名声明更容易理解: //FP是一个指向函数的指针的同义词,它指向的函数带有\n//int和const std::string&形参,不返回任何东西\ntypedef void (*FP)(int, const std::string&); //typedef //含义同上\nusing FP = void (*)(int, const std::string&); //别名声明 当然,两个结构都不是非常让人满意,没有人喜欢花大量的时间处理函数指针类型的别名(译注:指FP),所以至少在这里,没有一个吸引人的理由让你觉得别名声明比typedef好。 不过有一个地方使用别名声明吸引人的理由是存在的:模板。特别地,别名声明可以被模板化(这种情况下称为别名模板 alias template s)但是typedef不能。这使得C++11程序员可以很直接的表达一些C++98中只能把typedef嵌套进模板化的struct才能表达的东西。考虑一个链表的别名,链表使用自定义的内存分配器,MyAlloc。使用别名模板,这真是太容易了: template //MyAllocList是\nusing MyAllocList = std::list>; //std::list> //的同义词 MyAllocList lw; //用户代码 使用typedef,你就只能从头开始: template //MyAllocList是\nstruct MyAllocList { //std::list> typedef std::list> type; //的同义词 }; MyAllocList::type lw; //用户代码 更糟糕的是,如果你想使用在一个模板内使用typedef声明一个链表对象,而这个对象又使用了模板形参,你就不得不在typedef前面加上typename: template\nclass Widget { //Widget含有一个\nprivate: //MyAllocLIst对象 typename MyAllocList::type list; //作为数据成员 …\n}; 这里MyAllocList::type使用了一个类型,这个类型依赖于模板参数T。因此MyAllocList::type是一个依赖类型( dependent type ),在C++很多讨人喜欢的规则中的一个提到必须要在依赖类型名前加上typename。 如果使用别名声明定义一个MyAllocList,就不需要使用typename(同时省略麻烦的“::type”后缀): template using MyAllocList = std::list>; //同之前一样 template class Widget {\nprivate: MyAllocList list; //没有“typename” … //没有“::type”\n}; 对你来说,MyAllocList(使用了模板别名声明的版本)可能看起来和MyAllocList::type(使用typedef的版本)一样都应该依赖模板参数T,但是你不是编译器。当编译器处理Widget模板时遇到MyAllocList(使用模板别名声明的版本),它们知道MyAllocList是一个类型名,因为MyAllocList是一个别名模板:它 一定 是一个类型名。因此MyAllocList就是一个 非依赖类型 ( non-dependent type ),就不需要也不允许使用typename修饰符。 当编译器在Widget的模板中看到MyAllocList::type(使用typedef的版本),它不能确定那是一个类型的名称。因为可能存在一个MyAllocList的它们没见到的特化版本,那个版本的MyAllocList::type指代了一种不是类型的东西。那听起来很不可思议,但不要责备编译器穷尽考虑所有可能。因为人确实能写出这样的代码。 举个例子,一个误入歧途的人可能写出这样的代码: class Wine { … }; template<> //当T是Wine\nclass MyAllocList { //特化MyAllocList\nprivate: enum class WineType //参见Item10了解 { White, Red, Rose }; //\"enum class\" WineType type; //在这个类中,type是 … //一个数据成员!\n}; 就像你看到的,MyAllocList::type不是一个类型。如果Widget使用Wine实例化,在Widget模板中的MyAllocList::type将会是一个数据成员,不是一个类型。在Widget模板内,MyAllocList::type是否表示一个类型取决于T是什么,这就是为什么编译器会坚持要求你在前面加上typename。 如果你尝试过模板元编程( template metaprogramming ,TMP), 你一定会碰到取模板类型参数然后基于它创建另一种类型的情况。举个例子,给一个类型T,如果你想去掉T的常量修饰和引用修饰(const- or reference qualifiers),比如你想把const std::string&变成std::string。又或者你想给一个类型加上const或变为左值引用,比如把Widget变成const Widget或Widget&。(如果你没有用过模板元编程,太遗憾了,因为如果你真的想成为一个高效C++程序员,你需要至少熟悉C++在这方面的基本知识。你可以看看在 Item23 , 27 里的TMP的应用实例,包括我提到的类型转换)。 C++11在 type traits (类型特性)中给了你一系列工具去实现类型转换,如果要使用这些模板请包含头文件。里面有许许多多 type traits ,也不全是类型转换的工具,也包含一些可预测接口的工具。给一个你想施加转换的类型T,结果类型就是std::transformation::type,比如: std::remove_const::type //从const T中产出T\nstd::remove_reference::type //从T&和T&&中产出T\nstd::add_lvalue_reference::type //从T中产出T& 注释仅仅简单的总结了类型转换做了什么,所以不要太随便的使用。在你的项目使用它们之前,你最好看看它们的详细说明书。 尽管写了一些,但我这里不是想给你一个关于 type traits 使用的教程。注意类型转换尾部的::type。如果你在一个模板内部将他们施加到类型形参上(实际代码中你也总是这么用),你也需要在它们前面加上typename。至于为什么要这么做是因为这些C++11的 type traits 是通过在struct内嵌套typedef来实现的。是的,它们使用类型同义词(译注:根据上下文指的是使用typedef的做法)技术实现,而正如我之前所说这比别名声明要差。 关于为什么这么实现是有历史原因的,但是我们跳过它(我认为太无聊了),因为标准委员会没有及时认识到别名声明是更好的选择,所以直到C++14它们才提供了使用别名声明的版本。这些别名声明有一个通用形式:对于C++11的类型转换std::transformation::type在C++14中变成了std::transformation_t。举个例子或许更容易理解: std::remove_const::type //C++11: const T → T std::remove_const_t //C++14 等价形式 std::remove_reference::type //C++11: T&/T&& → T std::remove_reference_t //C++14 等价形式 std::add_lvalue_reference::type //C++11: T → T& std::add_lvalue_reference_t //C++14 等价形式 C++11的的形式在C++14中也有效,但是我不能理解为什么你要去用它们。就算你没办法使用C++14,使用别名模板也是小儿科。只需要C++11的语言特性,甚至每个小孩都能仿写,对吧?如果你有一份C++14标准,就更简单了,只需要复制粘贴: template using remove_const_t = typename remove_const::type; template using remove_reference_t = typename remove_reference::type; template using add_lvalue_reference_t = typename add_lvalue_reference::type; 看见了吧?不能再简单了。 请记住: typedef不支持模板化,但是别名声明支持。 别名模板避免了使用“::type”后缀,而且在模板中使用typedef还需要在前面加上typename C++14提供了C++11所有 type traits 转换的别名声明版本","breadcrumbs":"第三章 移步现代C++ » Item 9:优先考虑别名声明而非typedefs » 条款九:优先考虑别名声明而非typedefs","id":"22","title":"条款九:优先考虑别名声明而非typedefs"},"23":{"body":"Item 10: Prefer scoped enums to unscoped enums 通常来说,在花括号中声明一个名字会限制它的作用域在花括号之内。但这对于C++98风格的enum中声明的枚举名(译注: enumerator ,连同下文“枚举名”都指 enumerator )是不成立的。这些枚举名的名字(译注: enumerator names,连同下文“名字”都指names)属于包含这个enum的作用域,这意味着作用域内不能含有相同名字的其他东西: enum Color { black, white, red }; //black, white, red在 //Color所在的作用域\nauto white = false; //错误! white早已在这个作用 //域中声明 这些枚举名的名字泄漏进它们所被定义的enum在的那个作用域,这个事实有一个官方的术语:未限域枚举( unscoped enum )。在C++11中它们有一个相似物,限域枚举( scoped enum ),它不会导致枚举名泄漏: enum class Color { black, white, red }; //black, white, red //限制在Color域内\nauto white = false; //没问题,域内没有其他“white” Color c = white; //错误,域中没有枚举名叫white Color c = Color::white; //没问题\nauto c = Color::white; //也没问题(也符合Item5的建议) 因为限域enum是通过“enum class”声明,所以它们有时候也被称为枚举类( enum classes )。 使用限域enum来减少命名空间污染,这是一个足够合理使用它而不是它的同胞未限域enum的理由,其实限域enum还有第二个吸引人的优点:在它的作用域中,枚举名是强类型。未限域enum中的枚举名会隐式转换为整型(现在,也可以转换为浮点类型)。因此下面这种歪曲语义的做法也是完全有效的: enum Color { black, white, red }; //未限域enum std::vector //func返回x的质因子 primeFactors(std::size_t x); Color c = red;\n… if (c < 14.5) { // Color与double比较 (!) auto factors = // 计算一个Color的质因子(!) primeFactors(c); …\n} 在enum后面写一个class就可以将非限域enum转换为限域enum,接下来就是完全不同的故事展开了。现在不存在任何隐式转换可以将限域enum中的枚举名转化为任何其他类型: enum class Color { black, white, red }; //Color现在是限域enum Color c = Color::red; //和之前一样,只是\n... //多了一个域修饰符 if (c < 14.5) { //错误!不能比较 //Color和double auto factors = //错误!不能向参数为std::size_t primeFactors(c); //的函数传递Color参数 …\n} 如果你真的很想执行Color到其他类型的转换,和平常一样,使用正确的类型转换运算符扭曲类型系统: if (static_cast(c) < 14.5) { //奇怪的代码, //但是有效 auto factors = //有问题,但是 primeFactors(static_cast(c)); //能通过编译 …\n} 似乎比起非限域enum而言,限域enum有第三个好处,因为限域enum可以被前置声明。也就是说,它们可以不指定枚举名直接声明: enum Color; //错误!\nenum class Color; //没问题 其实这是一个误导。在C++11中,非限域enum也可以被前置声明,但是只有在做一些其他工作后才能实现。这些工作来源于一个事实:在C++中所有的enum都有一个由编译器决定的整型的底层类型。对于非限域enum比如Color, enum Color { black, white, red }; 编译器可能选择char作为底层类型,因为这里只需要表示三个值。然而,有些enum中的枚举值范围可能会大些,比如: enum Status { good = 0, failed = 1, incomplete = 100, corrupt = 200, indeterminate = 0xFFFFFFFF }; 这里值的范围从0到0xFFFFFFFF。除了在不寻常的机器上(比如一个char至少有32bits的那种),编译器都会选择一个比char大的整型类型来表示Status。 为了高效使用内存,编译器通常在确保能包含所有枚举值的前提下为enum选择一个最小的底层类型。在一些情况下,编译器将会优化速度,舍弃大小,这种情况下它可能不会选择最小的底层类型,而是选择对优化大小有帮助的类型。为此,C++98只支持enum定义(所有枚举名全部列出来);enum声明是不被允许的。这使得编译器能在使用之前为每一个enum选择一个底层类型。 但是不能前置声明enum也是有缺点的。最大的缺点莫过于它可能增加编译依赖。再次考虑Status enum: enum Status { good = 0, failed = 1, incomplete = 100, corrupt = 200, indeterminate = 0xFFFFFFFF }; 这种enum很有可能用于整个系统,因此系统中每个包含这个头文件的组件都会依赖它。如果引入一个新状态值, enum Status { good = 0, failed = 1, incomplete = 100, corrupt = 200, audited = 500, indeterminate = 0xFFFFFFFF }; 那么可能整个系统都得重新编译,即使只有一个子系统——或者只有一个函数——使用了新添加的枚举名。这是大家都 不希望 看到的。C++11中的前置声明enums可以解决这个问题。比如这里有一个完全有效的限域enum声明和一个以该限域enum作为形参的函数声明: enum class Status; //前置声明\nvoid continueProcessing(Status s); //使用前置声明enum 即使Status的定义发生改变,包含这些声明的头文件也不需要重新编译。而且如果Status有改动(比如添加一个audited枚举名),continueProcessing的行为不受影响(比如因为continueProcessing没有使用这个新添加的audited),continueProcessing也不需要重新编译。 但是如果编译器在使用它之前需要知晓该enum的大小,该怎么声明才能让C++11做到C++98不能做到的事情呢?答案很简单:限域enum的底层类型总是已知的,而对于非限域enum,你可以指定它。 默认情况下,限域枚举的底层类型是int: enum class Status; //底层类型是int 如果默认的int不适用,你可以重写它: enum class Status: std::uint32_t; //Status的底层类型 //是std::uint32_t //(需要包含 ) 不管怎样,编译器都知道限域enum中的枚举名占用多少字节。 要为非限域enum指定底层类型,你可以同上,结果就可以前向声明: enum Color: std::uint8_t; //非限域enum前向声明 //底层类型为 //std::uint8_t 底层类型说明也可以放到enum定义处: enum class Status: std::uint32_t { good = 0, failed = 1, incomplete = 100, corrupt = 200, audited = 500, indeterminate = 0xFFFFFFFF }; 限域enum避免命名空间污染而且不接受荒谬的隐式类型转换,但它并非万事皆宜,你可能会很惊讶听到至少有一种情况下非限域enum是很有用的。那就是牵扯到C++11的std::tuple的时候。比如在社交网站中,假设我们有一个 tuple 保存了用户的名字,email地址,声望值: using UserInfo = //类型别名,参见Item9 std::tuple ; //声望 虽然注释说明了tuple各个字段对应的意思,但当你在另一文件遇到下面的代码那之前的注释就不是那么有用了: UserInfo uInfo; //tuple对象\n…\nauto val = std::get<1>(uInfo);\t//获取第一个字段 作为一个程序员,你有很多工作要持续跟进。你应该记住第一个字段代表用户的email地址吗?我认为不。可以使用非限域enum将名字和字段编号关联起来以避免上述需求: enum UserInfoFields { uiName, uiEmail, uiReputation }; UserInfo uInfo; //同之前一样\n…\nauto val = std::get(uInfo); //啊,获取用户email字段的值 之所以它能正常工作是因为UserInfoFields中的枚举名隐式转换成std::size_t了,其中std::size_t是std::get模板实参所需的。 对应的限域enum版本就很啰嗦了: enum class UserInfoFields { uiName, uiEmail, uiReputation }; UserInfo uInfo; //同之前一样\n…\nauto val = std::get(UserInfoFields::uiEmail)> (uInfo); 为避免这种冗长的表示,我们可以写一个函数传入枚举名并返回对应的std::size_t值,但这有一点技巧性。std::get是一个模板(函数),需要你给出一个std::size_t值的模板实参(注意使用<>而不是()),因此将枚举名变换为std::size_t值的函数必须 在编译期 产生这个结果。如 Item15 提到的,那必须是一个constexpr函数。 事实上,它也的确该是一个constexpr函数模板,因为它应该能用于任何enum。如果我们想让它更一般化,我们还要泛化它的返回类型。较之于返回std::size_t,我们更应该返回枚举的底层类型。这可以通过std::underlying_type这个 type trait 获得。(参见 Item9 关于 type trait 的内容)。最终我们还要再加上noexcept修饰(参见 Item14 ),因为我们知道它肯定不会产生异常。根据上述分析最终得到的toUType函数模板在编译期接受任意枚举名并返回它的值: template\nconstexpr typename std::underlying_type::type toUType(E enumerator) noexcept\n{ return static_cast::type>(enumerator);\n} 在C++14中,toUType还可以进一步用std::underlying_type_t(参见 Item9 )代替typename std::underlying_type::type打磨: template //C++14\nconstexpr std::underlying_type_t toUType(E enumerator) noexcept\n{ return static_cast>(enumerator);\n} 还可以再用C++14 auto(参见 Item3 )打磨一下代码: template //C++14\nconstexpr auto toUType(E enumerator) noexcept\n{ return static_cast>(enumerator);\n} 不管它怎么写,toUType现在允许这样访问tuple的字段了: auto val = std::get(uInfo); 这仍然比使用非限域enum要写更多的代码,但同时它也避免命名空间污染,防止不经意间使用隐式转换。大多数情况下,你应该会觉得多敲几个(几行)字符作为避免使用未限域枚举这种老得和2400波特率猫同时代技术的代价是值得的。 记住 C++98的enum即非限域enum。 限域enum的枚举名仅在enum内可见。要转换为其它类型只能使用 cast 。 非限域/限域enum都支持底层类型说明语法,限域enum底层类型默认是int。非限域enum没有默认底层类型。 限域enum总是可以前置声明。非限域enum仅当指定它们的底层类型时才能前置。","breadcrumbs":"第三章 移步现代C++ » Item 10:优先考虑限域枚举而非未限域枚举 » 条款十:优先考虑限域enum而非未限域enum","id":"23","title":"条款十:优先考虑限域enum而非未限域enum"},"24":{"body":"Item 11: Prefer deleted functions to private undefined ones. 如果你写的代码要被其他人使用,你不想让他们调用某个特殊的函数,你通常不会声明这个函数。无声明,不函数。简简单单!但有时C++会给你自动声明一些函数,如果你想防止客户调用这些函数,事情就不那么简单了。 上述场景见于特殊的成员函数,即当有必要时C++自动生成的那些函数。 Item17 详细讨论了这些函数,但是现在,我们只关心拷贝构造函数和拷贝赋值运算符重载。本节主要致力于讨论C++98中那些被C++11所取代的最佳实践,而且在C++98中,你想要禁止使用的成员函数,几乎总是拷贝构造函数或者赋值运算符,或者两者都是。 在C++98中防止调用这些函数的方法是将它们声明为私有(private)成员函数并且不定义。举个例子,在C++ 标准库 iostream 继承链的顶部是模板类basic_ios。所有 istream 和 ostream 类都继承此类(直接或者间接)。拷贝 istream 和 ostream 是不合适的,因为这些操作应该怎么做是模棱两可的。比如一个istream对象,代表一个输入值的流,流中有一些已经被读取,有一些可能马上要被读取。如果一个 istream 被拷贝,需要拷贝将要被读取的值和已经被读取的值吗?解决这个问题最好的方法是不定义这个操作。直接禁止拷贝流。 要使这些 istream 和 ostream 类不可拷贝,basic_ios在C++98中是这样声明的(包括注释): template >\nclass basic_ios : public ios_base {\npublic: … private: basic_ios(const basic_ios& ); // not defined basic_ios& operator=(const basic_ios&); // not defined\n}; 将它们声明为私有成员可以防止客户端调用这些函数。故意不定义它们意味着假如还是有代码用它们(比如成员函数或者类的友元friend),就会在链接时引发缺少函数定义( missing function definitions )错误。 在C++11中有一种更好的方式达到相同目的:用“= delete”将拷贝构造函数和拷贝赋值运算符标记为 deleted 函数 (译注:一些文献翻译为“删除的函数”)。上面相同的代码在C++11中是这样声明的: template >\nclass basic_ios : public ios_base {\npublic: … basic_ios(const basic_ios& ) = delete; basic_ios& operator=(const basic_ios&) = delete; …\n}; 删除这些函数(译注:添加\"= delete\")和声明为私有成员可能看起来只是方式不同,别无其他区别。其实还有一些实质性意义。 deleted 函数不能以任何方式被调用,即使你在成员函数或者友元函数里面调用 deleted 函数也不能通过编译。这是较之C++98行为的一个改进,C++98中不正确的使用这些函数在链接时才被诊断出来。 通常, deleted 函数被声明为public而不是private。这也是有原因的。当客户端代码试图调用成员函数,C++会在检查 deleted 状态前检查它的访问性。当客户端代码调用一个私有的 deleted 函数,一些编译器只会给出该函数是private的错误(译注:而没有诸如该函数被 deleted 修饰的错误),即使函数的访问性不影响它是否能被使用。所以值得牢记,如果要将老代码的“私有且未定义”函数替换为 deleted 函数时请一并修改它的访问性为public,这样可以让编译器产生更好的错误信息。 deleted 函数还有一个重要的优势是 任何 函数都可以标记为 deleted ,而只有成员函数可被标记为private。(译注:从下文可知“任何”是包含普通函数和成员函数等所有可声明函数的地方,而private方法只适用于成员函数)假如我们有一个非成员函数,它接受一个整型参数,检查它是否为幸运数: bool isLucky(int number); C++有沉重的C包袱,使得含糊的、能被视作数值的任何类型都能隐式转换为int,但是有一些调用可能是没有意义的: if (isLucky('a')) … //字符'a'是幸运数?\nif (isLucky(true)) … //\"true\"是?\nif (isLucky(3.5)) … //难道判断它的幸运之前还要先截尾成3? 如果幸运数必须真的是整型,我们该禁止这些调用通过编译。 其中一种方法就是创建 deleted 重载函数,其参数就是我们想要过滤的类型: bool isLucky(int number); //原始版本\nbool isLucky(char) = delete; //拒绝char\nbool isLucky(bool) = delete; //拒绝bool\nbool isLucky(double) = delete; //拒绝float和double (上面double重载版本的注释说拒绝float和double可能会让你惊讶,但是请回想一下:将float转换为int和double,C++更喜欢转换为double。使用float调用isLucky因此会调用double重载版本,而不是int版本。好吧,它也会那么去尝试。事实是调用被删除的double重载版本不能通过编译。不再惊讶了吧。) 虽然 deleted 函数不能被使用,但它们还是存在于你的程序中。也即是说,重载决议会考虑它们。这也是为什么上面的函数声明导致编译器拒绝一些不合适的函数调用。 if (isLucky('a')) … //错误!调用deleted函数\nif (isLucky(true)) … //错误!\nif (isLucky(3.5f)) … //错误! 另一个 deleted 函数用武之地(private成员函数做不到的地方)是禁止一些模板的实例化。假如你要求一个模板仅支持原生指针(尽管 第四章 建议使用智能指针代替原生指针): template\nvoid processPointer(T* ptr); 在指针的世界里有两种特殊情况。一是void*指针,因为没办法对它们进行解引用,或者加加减减等。另一种指针是char*,因为它们通常代表C风格的字符串,而不是正常意义下指向单个字符的指针。这两种情况要特殊处理,在processPointer模板里面,我们假设正确的函数应该拒绝这些类型。也即是说,processPointer不能被void*和char*调用。 要想确保这个很容易,使用delete标注模板实例: template<>\nvoid processPointer(void*) = delete; template<>\nvoid processPointer(char*) = delete; 现在如果使用void*和char*调用processPointer就是无效的,按常理说const void*和const char*也应该无效,所以这些实例也应该标注delete: template<>\nvoid processPointer(const void*) = delete; template<>\nvoid processPointer(const char*) = delete; 如果你想做得更彻底一些,你还要删除const volatile void*和const volatile char*重载版本,另外还需要一并删除其他标准字符类型的重载版本:std::wchar_t,std::char16_t和std::char32_t。 有趣的是,如果类里面有一个函数模板,你可能想用private(经典的C++98惯例)来禁止这些函数模板实例化,但是不能这样做,因为不能给特化的成员模板函数指定一个不同于主函数模板的访问级别。如果processPointer是类Widget里面的模板函数, 你想禁止它接受void*参数,那么通过下面这样C++98的方法就不能通过编译: class Widget {\npublic: … template void processPointer(T* ptr) { … } private: template<> //错误! void processPointer(void*); }; 问题是模板特例化必须位于一个命名空间作用域,而不是类作用域。 deleted 函数不会出现这个问题,因为它不需要一个不同的访问级别,且他们可以在类外被删除(因此位于命名空间作用域): class Widget {\npublic: … template void processPointer(T* ptr) { … } … }; template<> //还是public,\nvoid Widget::processPointer(void*) = delete; //但是已经被删除了 事实上C++98的最佳实践即声明函数为private但不定义是在做C++11 deleted 函数要做的事情。作为模仿者,C++98的方法不是十全十美。它不能在类外正常工作,不能总是在类中正常工作,它的罢工可能直到链接时才会表现出来。所以请坚定不移的使用 deleted 函数。 请记住: 比起声明函数为private但不定义,使用 deleted 函数更好 任何函数都能被删除(be deleted),包括非成员函数和模板实例(译注:实例化的函数)","breadcrumbs":"第三章 移步现代C++ » Item 11:优先考虑使用deleted函数而非使用未定义的私有声明 » 条款十一:优先考虑使用 deleted 函数而非使用未定义的私有声明","id":"24","title":"条款十一:优先考虑使用 deleted 函数而非使用未定义的私有声明"},"25":{"body":"Item 12: Declare overriding functions override 在C++面向对象的世界里,涉及的概念有类,继承,虚函数。这个世界最基本的概念是派生类的虚函数 重写 基类同名函数。令人遗憾的是虚函数重写可能一不小心就错了。似乎这部分语言的设计理念是不仅仅要遵守墨菲定律,还应该尊重它。 虽然“重写( overriding )”听起来像“重载( overloading )”,然而两者完全不相关,所以让我澄清一下,正是虚函数重写机制的存在,才使我们可以通过基类的接口调用派生类的成员函数: class Base {\npublic: virtual void doWork(); //基类虚函数 …\n}; class Derived: public Base {\npublic: virtual void doWork(); //重写Base::doWork … //(这里“virtual”是可以省略的)\n}; std::unique_ptr upb = //创建基类指针指向派生类对象 std::make_unique(); //关于std::make_unique\n… //请参见Item21 upb->doWork(); //通过基类指针调用doWork, //实际上是派生类的doWork //函数被调用 要想重写一个函数,必须满足下列要求: 基类函数必须是virtual 基类和派生类函数名必须完全一样(除非是析构函数) 基类和派生类函数形参类型必须完全一样 基类和派生类函数常量性constness必须完全一样 基类和派生类函数的返回值和异常说明( exception specifications )必须兼容 除了这些C++98就存在的约束外,C++11又添加了一个: 函数的引用限定符( reference qualifiers )必须完全一样。成员函数的引用限定符是C++11很少抛头露脸的特性,所以如果你从没听过它无需惊讶。它可以限定成员函数只能用于左值或者右值。成员函数不需要virtual也能使用它们: class Widget {\npublic: … void doWork() &; //只有*this为左值的时候才能被调用 void doWork() &&; //只有*this为右值的时候才能被调用\n}; …\nWidget makeWidget(); //工厂函数(返回右值)\nWidget w; //普通对象(左值)\n…\nw.doWork(); //调用被左值引用限定修饰的Widget::doWork版本 //(即Widget::doWork &)\nmakeWidget().doWork(); //调用被右值引用限定修饰的Widget::doWork版本 //(即Widget::doWork &&) 后面我还会提到引用限定符修饰成员函数,但是现在,只需要记住如果基类的虚函数有引用限定符,派生类的重写就必须具有相同的引用限定符。如果没有,那么新声明的函数还是属于派生类,但是不会重写父类的任何函数。 这么多的重写需求意味着哪怕一个小小的错误也会造成巨大的不同。代码中包含重写错误通常是有效的,但它的意图不是你想要的。因此你不能指望当你犯错时编译器能通知你。比如,下面的代码是完全合法的,咋一看,还很有道理,但是它没有任何虚函数重写——没有一个派生类函数联系到基类函数。你能识别每种情况的错误吗,换句话说,为什么派生类函数没有重写同名基类函数? class Base {\npublic: virtual void mf1() const; virtual void mf2(int x); virtual void mf3() &; void mf4() const;\n}; class Derived: public Base {\npublic: virtual void mf1(); virtual void mf2(unsigned int x); virtual void mf3() &&; void mf4() const;\n}; 需要一点帮助吗? mf1在Base基类声明为const,但是Derived派生类没有这个常量限定符 mf2在Base基类声明为接受一个int参数,但是在Derived派生类声明为接受unsigned int参数 mf3在Base基类声明为左值引用限定,但是在Derived派生类声明为右值引用限定 mf4在Base基类没有声明为virtual虚函数 你可能会想,“哎呀,实际操作的时候,这些warnings都能被编译器探测到,所以我不需要担心。”你说的可能对,也可能不对。就我目前检查的两款编译器来说,这些代码编译时没有任何warnings,即使我开启了输出所有warnings。(其他编译器可能会为这些问题的部分输出warnings,但不是全部。) 由于正确声明派生类的重写函数很重要,但很容易出错,C++11提供一个方法让你可以显式地指定一个派生类函数是基类版本的重写:将它声明为override。还是上面那个例子,我们可以这样做: class Derived: public Base {\npublic: virtual void mf1() override; virtual void mf2(unsigned int x) override; virtual void mf3() && override; virtual void mf4() const override;\n}; 代码不能编译,当然了,因为这样写的时候,编译器会抱怨所有与重写有关的问题。这也是你想要的,以及为什么要在所有重写函数后面加上override。 使用override的代码编译时看起来就像这样(假设我们的目的是Derived派生类中的所有函数重写Base基类的相应虚函数): class Base {\npublic: virtual void mf1() const; virtual void mf2(int x); virtual void mf3() &; virtual void mf4() const;\n}; class Derived: public Base {\npublic: virtual void mf1() const override; virtual void mf2(int x) override; virtual void mf3() & override; void mf4() const override; //可以添加virtual,但不是必要\n}; 注意在这个例子中mf4有别于之前,它在Base中的声明有virtual修饰,所以能正常工作。大多数和重写有关的错误都是在派生类引发的,但也可能是基类的不正确导致。 比起让编译器(译注:通过warnings)告诉你想重写的而实际没有重写,不如给你的派生类重写函数全都加上override。如果你考虑修改修改基类虚函数的函数签名,override还可以帮你评估后果。如果派生类全都用上override,你可以只改变基类函数签名,重编译系统,再看看你造成了多大的问题(即,多少派生类不能通过编译),然后决定是否值得如此麻烦更改函数签名。没有override,你只能寄希望于完善的单元测试,因为,正如我们所见,派生类虚函数本想重写基类,但是没有,编译器也没有探测并发出诊断信息。 C++既有很多关键字,C++11引入了两个上下文关键字( contextual keywords ),override和final(向虚函数添加final可以防止派生类重写。final也能用于类,这时这个类不能用作基类)。这两个关键字的特点是它们是保留的,它们只是位于特定上下文才被视为关键字。对于override,它只在成员函数声明结尾处才被视为关键字。这意味着如果你以前写的代码里面已经用过 override 这个名字,那么换到C++11标准你也无需修改代码: class Warning { //C++98潜在的传统类代码\npublic: … void override(); //C++98和C++11都合法(且含义相同) …\n}; 关于override想说的就这么多,但对于成员函数引用限定( reference qualifiers )还有一些内容。我之前承诺我会在后面提供更多的关于它们的资料,现在就是\"后面\"了。 如果我们想写一个函数只接受左值实参,我们声明一个non-const左值引用形参: void doSomething(Widget& w); //只接受左值Widget对象 如果我们想写一个函数只接受右值实参,我们声明一个右值引用形参: void doSomething(Widget&& w); //只接受右值Widget对象 成员函数的引用限定可以很容易的区分一个成员函数被哪个对象(即*this)调用。它和在成员函数声明尾部添加一个const很相似,暗示了调用这个成员函数的对象(即*this)是const的。 对成员函数添加引用限定不常见,但是可以见。举个例子,假设我们的Widget类有一个std::vector数据成员,我们提供一个访问函数让客户端可以直接访问它: class Widget {\npublic: using DataType = std::vector; //“using”的信息参见Item9 … DataType& data() { return values; } …\nprivate: DataType values;\n}; 这是最具封装性的设计,只给外界保留一线光。但先把这个放一边,思考一下下面的客户端代码: Widget w;\n…\nauto vals1 = w.data(); //拷贝w.values到vals1 Widget::data函数的返回值是一个左值引用(准确的说是std::vector&), 因为左值引用是左值,所以vals1是从左值初始化的。因此vals1由w.values拷贝构造而得,就像注释说的那样。 现在假设我们有一个创建Widgets的工厂函数, Widget makeWidget(); 我们想用makeWidget返回的Widget里的std::vector初始化一个变量: auto vals2 = makeWidget().data(); //拷贝Widget里面的值到vals2 再说一次,Widgets::data返回的是左值引用,还有,左值引用是左值。所以,我们的对象(vals2)得从Widget里的values拷贝构造。这一次,Widget是makeWidget返回的临时对象(即右值),所以将其中的std::vector进行拷贝纯属浪费。最好是移动,但是因为data返回左值引用,C++的规则要求编译器不得不生成一个拷贝。(这其中有一些优化空间,被称作“as if rule”,但是你依赖编译器使用这个优化规则就有点傻。)(译注:“as if rule”简单来说就是在不影响程序的“外在表现”情况下做一些改变) 我们需要的是指明当data被右值Widget对象调用的时候结果也应该是一个右值。现在就可以使用引用限定,为左值Widget和右值Widget写一个data的重载函数来达成这一目的: class Widget {\npublic: using DataType = std::vector; … DataType& data() & //对于左值Widgets, { return values; } //返回左值 DataType data() && //对于右值Widgets, { return std::move(values); } //返回右值 … private: DataType values;\n}; 注意data重载的返回类型是不同的,左值引用重载版本返回一个左值引用(即一个左值),右值引用重载返回一个临时对象(即一个右值)。这意味着现在客户端的行为和我们的期望相符了: auto vals1 = w.data(); //调用左值重载版本的Widget::data, //拷贝构造vals1\nauto vals2 = makeWidget().data(); //调用右值重载版本的Widget::data, //移动构造vals2 这真的很棒,但别被这结尾的暖光照耀分心以致忘记了该条款的中心。这个条款的中心是只要你在派生类声明想要重写基类虚函数的函数,就加上override。 请记住: 为重写函数加上override 成员函数引用限定让我们可以区别对待左值对象和右值对象(即*this)","breadcrumbs":"第三章 移步现代C++ » Item 12:使用override声明重载函数 » 条款十二:使用override声明重写函数","id":"25","title":"条款十二:使用override声明重写函数"},"26":{"body":"Item 13: Prefer const_iterators to iterators STL const_iterator等价于指向常量的指针(pointer-to-const)。它们都指向不能被修改的值。标准实践是能加上const就加上,这也指示我们需要一个迭代器时只要没必要修改迭代器指向的值,就应当使用const_iterator。 上面的说法对C++11和C++98都是正确的,但是在C++98中,标准库对const_iterator的支持不是很完整。首先不容易创建它们,其次就算你有了它,它的使用也是受限的。假如你想在std::vector中查找第一次出现1983(C++代替C with classes的那一年)的位置,然后插入1998(第一个ISO C++标准被接纳的那一年)。如果 vector 中没有1983,那么就在 vector 尾部插入。在C++98中使用iterator可以很容易做到: std::vector values;\n…\nstd::vector::iterator it = std::find(values.begin(), values.end(), 1983);\nvalues.insert(it, 1998); 但是这里iterator真的不是一个好的选择,因为这段代码不修改iterator指向的内容。用const_iterator重写这段代码是很平常的,但是在C++98中就不是了。下面是一种概念上可行但是不正确的方法: typedef std::vector::iterator IterT; //typedef\ntypedef std::vector::const_iterator ConstIterT; std::vector values;\n…\nConstIterT ci = std::find(static_cast(values.begin()), //cast static_cast(values.end()), //cast 1983); values.insert(static_cast(ci), 1998); //可能无法通过编译, //原因见下 typedef不是强制的,但是可以让代码中的 cast 更好写。(你可能想知道为什么我使用typedef而不是 Item9 提到的别名声明,因为这段代码在演示C++98做法,别名声明是C++11加入的特性) 之所以std::find的调用会出现类型转换是因为在C++98中values是non-const容器,没办法简简单单的从non-const容器中获取const_iterator。严格来说类型转换不是必须的,因为用其他方法获取const_iterator也是可以的(比如你可以把values绑定到reference-to-const变量上,然后再用这个变量代替values),但不管怎么说,从non-const容器中获取const_iterator的做法都有点别扭。 当你费劲地获得了const_iterator,事情可能会变得更糟,因为C++98中,插入操作(以及删除操作)的位置只能由iterator指定,const_iterator是不被接受的。这也是我在上面的代码中,将const_iterator(我那么小心地从std::find搞出来的东西)转换为iterator的原因,因为向insert传入const_iterator不能通过编译。 老实说,上面的代码也可能无法编译,因为没有一个可移植的从const_iterator到iterator的方法,即使使用static_cast也不行。甚至传说中的牛刀reinterpret_cast也杀不了这条鸡。(它不是C++98的限制,也不是C++11的限制,只是const_iterator就是不能转换为iterator,不管看起来对它们施以转换是有多么合理。)不过有办法生成一个iterator,使其指向和const_iterator指向相同,但是看起来不明显,也没有广泛应用,在这本书也不值得讨论。除此之外,我希望目前我陈述的观点是清晰的:const_iterator在C++98中会有很多问题,不如它的兄弟(译注:指iterator)有用。最终,开发者们不再相信能加const就加它的教条,而是只在实用的地方加它,C++98的const_iterator不是那么实用。 所有的这些都在C++11中改变了,现在const_iterator既容易获取又容易使用。容器的成员函数cbegin和cend产出const_iterator,甚至对于non-const容器也可用,那些之前使用 iterator 指示位置(如insert和erase)的STL成员函数也可以使用const_iterator了。使用C++11 const_iterator重写C++98使用iterator的代码也稀松平常: std::vector values; //和之前一样\n…\nauto it = //使用cbegin std::find(values.cbegin(), values.cend(), 1983);//和cend\nvalues.insert(it, 1998); 现在使用const_iterator的代码就很实用了! 唯一一个C++11对于const_iterator支持不足(译注:C++14支持但是C++11的时候还没)的情况是:当你想写最大程度通用的库,并且这些库代码为一些容器和类似容器的数据结构提供begin、end(以及cbegin,cend,rbegin,rend等)作为 非成员函数 而不是成员函数时。其中一种情况就是原生数组,还有一种情况是一些只由自由函数组成接口的第三方库。(译注:自由函数 free function ,指的是非成员函数,即一个函数,只要不是成员函数就可被称作 free function )最大程度通用的库会考虑使用非成员函数而不是假设成员函数版本存在。 举个例子,我们可以泛化下面的findAndInsert: template\nvoid findAndInsert(C& container, //在容器中查找第一次 const V& targetVal, //出现targetVal的位置, const V& insertVal) //然后在那插入insertVal\n{ using std::cbegin; using std::cend; auto it = std::find(cbegin(container), //非成员函数cbegin cend(container), //非成员函数cend targetVal); container.insert(it, insertVal);\n} 它可以在C++14工作良好,但是很遗憾,C++11不在良好之列。由于标准化的疏漏,C++11只添加了非成员函数begin和end,但是没有添加cbegin,cend,rbegin,rend,crbegin,crend。C++14修订了这个疏漏。 如果你使用C++11,并且想写一个最大程度通用的代码,而你使用的STL没有提供缺失的非成员函数cbegin和它的朋友们,你可以简单的写下你自己的实现。比如,下面就是非成员函数cbegin的实现: template