Modern Effective Cpp 阅读笔记
https://github.com/CnTransGroup/EffectiveModernCppChinese
条款一:理解模板类型推导
TL;DR 总是 pass-by-value,因此值(或指针)本身的 cv 属性不保留,引用也不会被保留,如果内部类型的声明中有 cv 类型,则再补上。通用引用 + 传左值 是例外,此时内部类型是左值引用
1 |
|
- 情景一:ParamType是一个指针或引用,但不是通用引用
expr 忽略引用部分,然后与 ParamType 模式匹配得到 T - 情景二:ParamType是一个通用引用
如果 expr 是左值,T 和 ParamType 都会被推导为左值引用
如果 expr 是右值,走情景一的规则 - 情景三:ParamType既不是指针也不是引用
当ParamType既不是指针也不是引用时,我们通过传值(pass-by-value)的方式处理
传值意味着拷贝,意味着 cv 属性失效,当然 ref-to-const、ptr-to-const 不会失效
1 |
|
条款二:理解auto类型推导
auto 类型推导与模板类型推导基本一致,区别在于模板类型推导无法处理 uniform initialization 的情况,需要在参数中手动写上 std::initializer_list<T>
才行
此外 C++14 lambda 函数允许形参使用 auto,这里使用的是模板类型推导那一套
1 |
|
条款三:理解decltype
在C++11中,decltype最主要的用途就是用于声明函数模板,而这个函数返回类型依赖于形参类型。
1 |
|
问题在于通常容器的 []
操作符返回的是引用,而 auto 走模板推导的话会忽略引用,这使得(假设是一个 int 容器)我们返回的是一个 int(右值),无法修改容器内的值。我们希望能够完全用 authAndAccess(c, i) = 1
替代 c[i] = 1
的效果
C++14通过使用 decltype(auto)
说明符使得这成为可能,auto 代表这个类型需要推导,默认使用 auto 对应的模板推导规则,而 decltype(auto) 意味着使用 decltype 对应的规则,意味着引用(与 cv 属性)会被保留。
1 |
|
另外,对于T类型的不是单纯的变量名的左值表达式,decltype总是产出T的引用即T&
1 |
|
条款四:学会查看类型推导结果
观察类型(包括 cv 属性)的方法:IDE、编译器、运行时输出
1 |
|
条款五:优先考虑auto而非显式类型声明
- auto 变量确保必须被初始化
- 只有编译器知道类型的变量,用 auto 表示会更简单
1 |
|
- 使用 auto 指向可调用类型比用
std::function
效率更高(对象必定存在栈上) - 确保类型正确
std::vector::size
的返回值不一定与 unsigned 相同,使用 auto 我们则不必考虑这个问题std::unordered_map<std::string, int>
在用 pair 遍历时(由于 key 是 const 的),类型实际是std::pair<const std::string, int>
,误用非 const 版本的 pair 进行遍历会导致拷贝,并且会导致在临时变量上修改
- 重构时不用手动改 auto 变量的类型
条款六:auto推导若非己愿,使用显式类型初始化惯用法
不可见的代理类可能会使auto从表达式中推导出“错误的”类型
1 |
|
这里的问题在于,std::vector<bool>
的 operator[]
返回的是一个std::vector<bool>::reference
的对象。
C++ 禁止对 bits 的引用,因此无法返回一个bool&
。std::vector<bool>
的 operator[]
返回一个行为类似于 bool&
的对象(上面提到的 reference 对象),之后再隐式转换为 highPriority 要求的 bool 类型。
std::vector<bool>::reference
是一个代理类(proxy class)的例子:所谓代理类就是以模仿和增强一些类型的行为为目的而存在的类
当类型声明为 auto 时,一种可能的情况是,这个 bit reference 存的是指向 vector 中某个字节的指针,以及这个指针中我们需要第几位,但由于 features 函数返回的 vector 只是一个临时变量,有可能在使用 highPriority 这个 reference 的时候真正指向的 vector 内存已经被销毁,产生 UB
另一个使用 auto
+ static_cast
的场景是需要手动转换类型,并且相对于 int x = func_return_float()
的写法更能表示“我希望做类型转换”的目的,后者无法看出(不考虑函数名)函数的返回值可能不是 int 类型
条款七:区别使用()和{}创建对象
C++11 使用花括号来统一初始化方式
- 与 auto 搭配时需要注意类型推导
- 不允许非兼容(变窄)转换
- 若有
std::initializer_list
构造函数,那么会优先尝试将花括号内元素转为 list 声明的类型,如果引入非兼容转换,会出现编译错误,如果无法转换,那么才考虑其他构造函数 type name();
是函数声明,type name{};
是变量声明,且不会调用 list 构造函数- 要么统一使用圆括号,要么统一使用化括号
条款八:优先考虑 nullptr 而非 0 和 NULL
0 或 NULL 都存在与整数类型重载混淆的问题
1 |
|
nullptr
可以隐式转换为所有类型的指针,同时它也不会匹配到整形的函数重载,也不会被模板类/函数推导为整形(which is the case of 0 or NULL)
条款九:优先考虑别名声明而非typedefs
TODO
总之优先用 using <typeNameAlias> = xxx
而别用 typedef
条款十:优先考虑限域enum而非未限域enum
- C++11 引入了 scoped enum,解决了以前 enum 中的元素实际上会占用 enum 被定义的空间内的名称的问题
- scoped enum 视为强类型(但可以 static_cast 显式转换),而 unscoped enum 会被隐式转换为整形
- C++11 中 scoped enum 可以前置声明,使用时引用此声明(而非引用完整定义)。若 enum 修改,不会导致所有引用到的地方都重新编译、此外也有助于优化头文件的可读性
- unscoped enum 没有默认底层类型,而 scoped enum 默认底层类型是 int,可以通过
enum class : <underlying type>
来指定底层类型
1 |
|
条款十一:优先考虑使用 deleted 函数而非使用未定义的私有声明
有时我们不希望用户调用某个特殊的函数(这里特指 拷贝构造函数或者赋值运算符)
在C++98中防止调用这些函数的方法是将它们声明为私有(private)成员函数并且不定义,而 C++11 起的推荐做法是用 deleted。它不止能应用于成员函数,也可以引用于普通函数、函数模板(举个例子,某个模板函数可以接收 int、char 以外的所有类型,就可以用 deleted 实现)
1 |
|
条款十二:使用override声明重写函数
用 override 声明重写函数,可以让编译器帮助我们检查是否真的重写了父类的函数
这里额外列一下重写函数的规则:
- 基类函数必须是virtual
- 基类和派生类函数名必须完全一样(除非是析构函数)
- 基类和派生类函数形参类型必须完全一样
- 基类和派生类函数常量性constness必须完全一样
- 基类和派生类函数的返回值和异常说明(exception specifications)必须兼容
- (C++11) 函数的引用限定符(reference qualifiers)必须完全一样。引用限定的左右值区分了调用函数时的 this 指针是左值还是右值
1 |
|