《Effective Modern C++》读后感

前言

前几天学校图书馆进了一批新书,看了下,刚好有《Effective Modern C++》这本书,这本书一直想看,于是借来看了下。(PS:总共三本,一开始还是借不到的,只能预约,蛮意外的,原来我们学校有这么多人学C++吗,毕竟班里其他人都学的是Java……

开头

献给爱犬,嗯,不过这次少了张图。关于C++11/14,在我之前写一个项目里用过并熟悉了下,所以这本书除了并发那里基础知识都没什么问题,那么来看看C++11/14又有什么坑呢?

型别推导

推导,这一听就是模板了,虽然日常编码中模板用得是比较少的,不过看一看还是很有意思的:

  • C++11中,模板型别推导多了一个万能引用 T&&,并且我还学到了数组和函数的指针退化,并且在引用情况下并不会退化;
  • auto和模板推导基本一致,除了大括号表达式;
  • decltype,恰如其名,就是准确推断类型,用来修饰auto就可以改变推导规则;
  • 查看推导结果,这还是信不过编译器吧,不过有时候这也是没办法的; 要点速记:
  • 模板型别推导过程中,引用性会被忽略;
  • 按值形参推导时,会忽略const和volatile;
  • 模板型别推导过程中,如果不是引用,数组和函数会退化为指针;
  • auto规则与模板型别推导一致,除了大括号初始化(initializer_list);
  • C++14里,auto用于函数返回值和lambda形参使用模板型别推导而不是auto推导;
  • 对于左值表达式类型T,decltype推导出T&;
  • decltype(auto)改变auto推导规则为decltype规则;

auto

之前我几乎不用auto,我对这种东西是不放心的,不过看了这章,我感觉我可以多用一点auto了:

  • auto有挺多好处,但也有一些坏处,不过auto推导的类型都能用IDE看,所以可以稍微放心;
  • 利用显式型别也就是强制转换来帮助auto推导,我觉得蛮蠢的:(

要点速记:

  • auto首先是少打字,能防止未初始化,保证你的变量类型正确,避免一些兼容问题,还能简化重构,少犯点错;
  • 代理类会让auto推导出错误的类型;

现代C++

这一章讨论了现代C++的一些做法,可以避免原来的一些问题,让你的代码更modern:)

  • 我倾向用()初始化,除非是想用initializer_list,至于防止窄型型别转换,这个自己注意就好;
  • 建议nullptr,没什么好说的;
  • using可读性更高,TMP那边的typename, ::type是很恶心,不过写得少,总之,用using;
  • enum class显然更好,类型安全,不污染命名空间;
  • delete还能删除普通函数的,受教了;
  • this还能区分左右值,基本没见过,不过现在明白了:)
  • 只要能const就const,迭代器也是;
  • 异常安全……希望以后能深入,还是很迷惑,真要做到异常安全太难了;
  • constexpr,这是要累死编译器;
  • C++11里面新加了移动函数两兄弟了,编译器会做更多事了:(

要点速记:

  • 函数重载决议时,{}初始化几乎总会和initialize_list匹配;
  • 对于vector这种,()和{}初始化差别很大,这种接口设计是很失败的;
  • 在模板里,用()还是{}是个问题,建议文档描述;
  • 用nullptr而不是NULL;
  • 用using而不是typedef;
  • 尽可能用enum class,除非没办法;
  • 用delete和default;
  • override和final可以用一用;
  • 优先用const_iterator;
  • 能加noexcept就加;
  • constexpr都是const,反之不一定;
  • constexpr函数如果传入编译期值就返回编译期值,否则和普通函数一样;
  • 编译期值某种意义上总是好的,虽然编译慢了点;
  • 只有类中不显式声明复制操作,移动操作,析构函数,移动操作才会由编译器自己生成;
  • 如果声明了移动函数,那么复制函数会被删除;
  • 成员函数模板不会影响复制和移动成员函数的生成;

智能指针

智能指针能避免资源泄露,但要明白,本质是要理解资源所用权,裸指针没有资源所用权,为此还有个observer_ptr表达这个东西,不过这东西褒贬不一,我是觉得这东西还行:

  • RAII思想是十分重要的,这也是要明白资源所有权问题,嗯……我感觉我应该抽时间学学Rust了;
  • make_unique竟然是在C++14里加进去的,看不明白:(

要点速记:

  • unique_ptr是独占所有权;
  • unique_ptr的删除器类型是模板参数,而shared_ptr是个属性;
  • shared_ptr是共享所有权;
  • shared_ptr有个控制块,这个控制块是实现weak_ptr和shared_form_this的关键;
  • shared_ptr的空间占用是比较大的;
  • weak_ptr没有资源所有权,不过比起裸指针,它能知道资源是否还活着;
  • 优先用make_unique和make_shared,少用new;
  • make_shared能降低内存分配次数,但有时候这会导致内存占用过大;
  • 对于unique_ptr,pImpl手法需要声明很多特种成员函数;

右值引用,移动语义,完美转发

左右值我感觉是C++11里相当重要的概念了,至于完美转发,总觉得很丑陋……来看看有意思的地方:

  • move和forward真的移动和转发了吗?没有;
  • 万能引用万能吗?嗯……其实是引用折叠;
  • 完美转发完美吗?不完美;
  • 万能引用虽说能尽可能压榨性能,但重载时遇到的问题,解决方案也太……;
  • 移动好像并没有我想象中那么高效;

要点速记:

  • move和forward不过是编译期类型强制转换而已,运行期它们什么事也不做;
  • 万能引用必须是T&&形式,而且必须涉及到型别推导;
  • 用右值初始化万能引用得到右值引用,用左值初始化万能引用得到左值引用;
  • 对右值引用或万能引用的最后一次使用再move或forward;
  • 如果局部对象有可能RVO,就不要move或forward了;
  • 最好不要把万能引用用作重载,它太能匹配了,转发构造函数时的问题最严重;
  • 非要把万能引用用作重载的话,可以考虑传值、标签分配、enable_if等TMP方式;
  • 引用折叠在这四种语境中发生:模板实例化、auto型别推导、using和typedef、decltype;
  • 引用折叠时,只要有一个左值引用,折叠结果就是左值引用,都是右值引用折叠结果才是右值引用;
  • 完美转发失败含义:模板推导失败、模板推导结果错误;
  • 完美转发失败场景:{}初始化、0或NULL的空指针、仅有声明的static const成员变量、模板或重载函数名称、位域;

lambda表达式

lambda表达式是个好东西,写起来很方便,配合function来用更是方便,不过当成闭包来用,又有很多坑……

要点速记:

  • 不要用默认捕获;
  • 引用捕获注意引用失效,按值捕获一个引用类型的值也要注意;
  • 初始化捕获可以将对象移入闭包;
  • 在lambda里用到auto&&时,用decltype来forward;
  • 尽可能用lambda,而不是bind;

并发

并发的话,我一直是用pthread的,而c++11的并发库,只稍微看过一些API,没实际用过,看网上说也不是太好,因此这章就稍微看看吧:(

要点速记:

  • 优先选用async;
  • async有两种启动策略,launch::deferred是推迟执行,launch::async才是真异步;
  • thread对象必须join;
  • future通常简单析构,而由async返回的future,在最后一个future析构时,会隐式join底层线程;
  • promise……看着感觉像golang的管道;
  • 分清atomic和volatile,这两个东西是完全不同的;

微调

这一章没什么好说的,讲了一些选择性的行为。

要点速记:

  • 对于可复制、移动成本低的形参,可以考虑按值传递然后move;
  • emplace有时会比push/insert快,比如添加的值是以构造形式加入容器、实参类型与容器类型不符、容器没有拒绝重复key机制;
  • emplace在类型转换那里有坑;

总结

总的来看,我从这本书里主要学到的东西是关于模板方面的,比如说型别推导、move、forward、auto这种,当然也让我对左右值理解更深了,也加深了很多C++11特性的印象。虽然这些东西我感觉以后用不上,不过看书总归是没错的,并且我要感叹,C++还是太难了:(