0%

Effective Modern C++读书笔记(一)

开个新坑,把读Effective Modern C++的读书笔记记录一下,与以往的形式不同这次是读完书很久再回头重读,预计通过五六篇博文来写这次总共42条条目的内容。
本文包含《Effective Modern C++》中第1、2章内容的个人笔记,讨论现代c++中型别自动推导相关内容。

第1章 型别推导

C++11引入了auto和decltype型别推导关键字,并在14标准中扩充完善了这两个关键字的使用场景。本章的目的就在于厘清这两个关键字的用法,并明确编译器在推导型别时的行为。

条款1:理解模版型别推导

由于auto绝大多数情况下的推导行为和模版型别推导一致,所以需要先明确的是模版型别推导规则。
以下列代码为例:

1
2
3
template<typename T>
void f(ParamType param);
f(expr);

分三种情况讨论T和ParamType的型别推导问题
情况1: ParamType是个指针或引用,但不能是个万能引用
第一种情况是符合直觉的也是最简单的情况,没有什么需要特殊注意的。
情况2: ParamType是个万能引用
万能引用场景下,传递左值实参则ParamType会被推导为左值且T的类型会被推导为引用,传递右值则ParamType会被推导为右值且T不会被推导为引用。
情况3: ParamType既非指针也非引用
亦即按值传递场景,通常也是符合直觉的,不过需要注意的是如果ParamType不具备const或volatile属性,而expr具备该属性,那么ParamType依然会被推导为不具备const或volatile属性,也就是传递了实参的副本。
另外书中介绍的是两个边缘场景,当数组和函数作为实参的场景。这两个场景中数组和函数将退化为指针,但是在ParamType为引用的场景时T的型别将被推导为实际的数组型别,也就是传递了一个数组引用,这里书上也介绍了一个通过模版来获取实际数组长度的方法。

条款2: 理解auto型别推导

auto关键字在绝大多数场景下的型别推导行为和模版型别推导行为一致,当某变量采用auto来声明时,auto就扮演了模版中的T这个角色,而变量的型别饰词则扮演了ParamType的角色。

1
2
3
4
5
6
7
8
9
10
11
12
auto x = 27;                // 对应条款1中的情况3,x既非指针也非引用
const auto cx = x; // 同上
const auto& rx = x; // 情况1,rx是引用但非万能引用
auto&& uref1 = x; // 情况2,uref1型别为int&
auto&& uref2 = cx; // uref2型别为const int&
auto&& uref3 = 27; // uref3型别为int&&
const char name[] = “R.N. Briggs”;
auto arr1 = name; // arr1型别为const char*
auto& arr2 = name; // arr2型别为const char(&)[13]
void someFunc(int, double);
auto func1 = someFunc; // func1型别为void (*)(int, double)
auto& func2 = someFunc; // func2型别为void (&)(int, double)

auto型别推导与模版型别推导唯一的不同在于统一初始化上,auto的推导有条特殊规则。当用户auto声明的变量的初始化表达式是使用大括号括起来时,推导所得的型别就属于std::initializer_list。也就是说对于使用大括号统一初始化的auto变量,编译器先默认该类型为std::initializer_list,再推导其内的类型。
另外需要注意的是C++14标准中扩充了auto的使用场景,在函数返回值和lambda的形参上也可以使用auto进行声明,而这样声明编译器的推导行为是按模版型别推导而非auto型别推导,也就是说在以上两种情况中如果返回了或者传递了大括号统一初始化式表达式,编译将无法正常执行。

条款3: 理解decltype

decltype在绝大多数情况下,得到的型别与表达式完全一致。不过需要注意的是,如果是比仅有名字更复杂的左值表达式,decltype则保证得出的型别总是左值引用,如书上所说的

1
int x = 0;

decltype((x))的结果为int&。
另外就是c++14中的decltype(auto),由于c++14标准允许将一般函数的返回值声明为auto,而如果此函数返回的是一个引用,则会导致实际上返回的是一个对象。这是由于在条款2中所说的将一个引用传递给一个单纯的auto声明变量,将导致实际上声明一个新值,所以c++14标准中出现了decltype(auto)的用法,用于表明做自动推导的同时按decltype规则来确定最终型别,亦即可以产生引用型别。

条款4: 掌握查看型别推导结果的方法

这里梅耶斯介绍了三个不同阶段查看型别推导结果的方法,通过IDE、编译器诊断信息、运行时std::type_info或者boost::type_index。
IDE工具通常会在我们编写代码时执行编译一次,所以简单的一些型别推导在IDE内就能看到,但是对于复杂的型别就不太有用。
第二种方式是通过定义一个未实现的模版,来通过编译器的报错信息来查看型别推导结果,即

1
2
3
template<typename T>
class TD;
TD<decltype(x)> xType;

编译器编译过程中则会报出类似TD相应类型未做声明的错误。
第三种方式在运行期,通过std::type_info来查看结果:

1
typeid(x).name()

由于标准规定“std::type_info::name中处理型别的方式就仿佛是向函数模版按值传递形参一样”,导致其结果不一定完全正确。
或者通过boost::type_index来查看:

1
boost::typeindex::type_id_with_cvr<T>().pretty_name()

第2章 auto

本章主要讨论auto的优缺点以及可能出现的问题

条款5: 优先选用auto,而非显示型别声明

auto变量必须初始化,这也就是避免了因为没有初始化变量而造成的问题,同时auto不会出现型别不匹配的问题,避免由该问题造成的兼容性和效率问题,同时在重构代码时也会减少改动。
这里书上提到了一个问题,即关于lambda表达式的声明问题。显而易见的是显式声明std::function将多敲很多代码,不过更值得注意的是通过std::function来声明lambda对象将带来性能问题。按照书上的说法,auto声明的lambda表达式与function不同,function的构造函数如果因为存储的闭包内存不够时会分配堆上的内存来进行存储,同时function的编译器实现一般会限制内联,产生间接函数调用,使function手法比起auto手法又大又慢。

条款6: 当auto推导的型别不符合要求时,使用带显式型别的初始化物习惯用法

auto在应对“隐形”代理型别时可能引发问题,书上以vector<bool>为例,vector<bool>在stl实现中是vector的一个特化版本,其operator[]方法返回的并不是T&即bool&,而是vector<bool>::reference,导致使用auto声明vector<bool>::operator[]返回值变量时将得到vector<bool>::reference,而程序员此时以直觉来判断这个变量应该是bool,则会在接下来的代码中出现型别不一致问题。这就是梅耶斯所说的“隐形”代理型别导致的问题,其实也就是因为auto推导的型别和我们认为的型别不一致而导致的问题,这类问题通常也只能在编译和测试时才能发现。
当我们发现此类问题时,修改方式不必是放弃auto,梅耶斯这里提出可以显式进行强转并保留auto,从而保留部分在条款5中提到的优点。如上提到的vector<bool>的问题可以改为

1
2
std::vector<bool> features(const Widget& w);
auto highPriority = static_cast<bool>(features(w)[5]);

当然这种写法也可以用于显式表明类型转换,如

1
2
double calcEpsilon();
auto ep = static_cast<float>(calcEpsilon());