Item7: 创建对象时区分开()和{}
通常来说,C++11中我们能用()、{}和=来初始化一个变量:
| 1 | int x(0); | 
但这几种初始化方式之间还有着区别。
=与另外两个不太一样,它代表着拷贝构造函数或赋值函数。
{}是C++11引入的新的初始化方式,它被设计为能用在各种地方,表达各种形式的值,也可以称为“统一初始化式”。
它能表达一组值,来初始化STL容器:
| 1 | std::vector<int> v{1, 3, 5}; | 
它能用来给类的非static成员设定默认值(而()就不行):
| 1 | class Widget { | 
它和()都能用于初始化一个uncopyable的对象(而=就不行):
| 1 | std::atomic<int> ai1{0}; // fine | 
{}有一个性质:它会阻止基本类型向下转换:
| 1 | double x, y, z; | 
另一个性质是:它不会被认为是声明。
C++中规定“所有看起来像声明的语句都会被视为声明”,这导致()在一些场景下会被视为函数声明,而{}则不会:
| 1 | Widget w1(10); // call Widget ctor with 10 | 
但是{}也不是什么都好,在类有std::initializer_list参数的构造函数时,{}会有麻烦:{}总会被认为是std::initializer_list,即使解析出错。
我们先看一个没有std::initializer_list构造函数的类Widget:
| 1 | class Widget { | 
一切都很正常,直到我们给Widget添加了一个新构造函数:
| 1 | class Widget { | 
甚至通常拷贝和移动构造函数该被调用的地方,都会被劫持到std::initializer_list构造函数上:
| 1 | class Widget { | 
甚至在{}中的内容没办法完全匹配std::initializer_list时:
| 1 | class Widget { | 
只有当{}中的所有元素都没办法转换为std::initializer_list需要的类型时,编译器才会去选择其它构造函数。比如上面的bool改为std::string,编译器找不到{10, 5.0}中有能转换为std::string的元素,就会去匹配我们希望的前两个构造函数了。
一个有趣的地方:如果{}中没有元素,那么被调用的是默认构造函数,而不是一个空的std::initializer_list。
如果你真的想传入一个空的std::initializer_list,那么这样:
| 1 | Widget w4({}); | 
std::vector<int>就有上面说的问题:
| 1 | std::vector<int> v1(10, 20); // use non-std::initializer_list ctor: | 
从这个问题中,我们能学到什么?
首先,向一个已有的类添加std::initialzier_list构造函数要非常谨慎,这可能会导致用户的调用被劫持。
其次,作为用户的我们,要小心选择用()还是{}。{}的好处上面已经说了。()的好处是:与C++98风格的连续性,还能避免陷入std::initializer_list的问题。
当你写模板代码时,如果需要创建一个对象,你会用哪种语法?
| 1 | T localObject(std::forward<Ts>(params)...); | 
如果T是std::vector<int>,参数是{10, 20},哪种是对的?只有用户才知道。
std::make_unique和std::make_shared遇到了这个问题,它们选择了(),并在文档中说明了这个选择。
有一种方法允许用户来指定用()还是{}:Intuitive interface。
(我觉得只在{}明确有好处的地方用{},其它地方还是用()比较好)
Item8: 优先选用nullptr来替代0和NULL
首先是结论:
- NULL是0,0是- int,不是指针;
- nullptr不是指针,但可以安全地用在需要用指针的场合;
- 因此在需要空指针的地方,用nullptr。
- 用nullptr还能提高代码的可读性。
NULL和0不是指针会带来什么问题?函数重载。当一个函数同时有int参数和指针参数的两个重载版本时,传入NULL或0(下面只说NULL吧,反正它们两个是完全一样的),你猜编译器会匹配哪个版本?
| 1 | void f(int); | 
f(NULL)能否编译过取决于NULL被定义成了什么:如果定义为0,就能编译过,调用f(int);如果定义为0L,那么NULL的类型就是long,编译器会发现f没有直接匹配long的重载版本,但有两个相同优先级的转换后匹配f(int)和f(void*),于是就报错。
C++98中我们只能尽量避免同时存在整数参数和指针参数的重载版本,但C++11中我们可以用nullptr。
nullptr不是指针,它的类型是std::nullptr_t,但C++11保证它能转换为任意的裸指针类型。
上面的例子中,f(nullptr)就会乖乖地调用f(void*)。
nullptr还能让代码更清晰,尤其是和auto配合使用时。假设你看到这么一段代码:
| 1 | auto result = findRecord(/* args */); | 
如果你不看findRecord的签名,就没办法区分result是整数还是指针。但如果用nullptr就很明显的表达了result是指针:
| 1 | auto result = findRecord(/* args */); | 
在有模板出现的地方,nullptr更加有优势。看下面的例子,我们有三个函数,都需要持有相应的锁时才能调用:
| 1 | int f1(std::shared_ptr<Widget> spw); | 
刚开始代码是这样的:
| 1 | std::mutex f1m, f2m, f3m; // mutexes for f1, f2, and f3. | 
上面这段代码能工作,但是丑了点:三段代码的模式都是一样的,应该封装起来。鉴于f1、f2、f3的参数和返回类型都不一样,我们需要用模板来实现:
| 1 | template <typename FuncT, typename MuxT, typename PtrT> | 
C++14中我们还可以用decltype(auto)作为返回值类型,但不影响结论。
现在我们来试一下新代码:
| 1 | auto r1 = lockAndCall(f1, f1m, 0); // error! PtrT is deduced to int | 
前两个调用是错的!模板只能看到int,而shared_ptr和unique_ptr没有int参数的构造函数。第三个调用则没有任何问题,PtrT被推断为std::nullptr_t,在调用f3时隐式转换为Widget*。
我们再回头看一下本节的结论,在下个需要传入空指针的地方,你还会用0或NULL吗?
Item9: 优先选用using来替代typedef
大家在用STL时,经常会搞出来std::unique_ptr<std::unordered_map<std::string, std::string>>这么长的类型出来。为了避免写好几次这么长的类型,我们会用typedef将它定义为一个新类型:
| 1 | typedef std::unique_ptr<std::unordered_map<std::string, std::string>> UPtrMapSS; | 
而在C++11中,我们多了一种选择:可以用using做类似的事:
| 1 | using UPtrMapSS = std::unique_ptr<std::unordered_map<std::string, std::string>>; | 
那么为什么在有typedef的情况下C++还要再增加一个using呢?
首先,using在表达类型时更清晰:
| 1 | typedef void (*FP)(int, const std::string&); | 
typedef来自于C,它的设计初衷是想用声明变量的方式来声明一个类型,但这导致名字和类型搅合在了一起,严重的影响了可读性。而using则将名字和类型分开了,我们能更容易的看出我们声明的类型到底是什么。
其次,using能模板化,称为”别名模板”,而typedef不能:
| 1 | template <typename T> | 
C++98中我们可以通过struct来绕过这个问题:
| 1 | template <typename T> | 
但在模板里我们不能直接使用这个类型,要在前面加上typename:
| 1 | template <typename T> | 
原因是编译器在第一次处理模板时,不知道T是什么,也没办法知道MyAllocList<T>::type是什么,它只能假设type是MyAllocList<T>的一个成员。只有前面加上typename编译器才能放心的将type按类型处理。
而using则明确表明它就是声明了一个类型,因此不需要加typename。
做过一些模板元编程(TMP)的人应该都有体会,在处理类型时,我们经常要用到下面的trait类:
| 1 | std::remove_const<T>::type // const T -> T | 
但每次使用都要在前面加上typename,就是因为这些trait类都是通过typedef定义出来的。
C++14中这些类都有了一个using版本,直接是一个类型,使用时不需要加typename:
| 1 | std::remove_const<T>::type -> std::remove_const_t<T> | 
如果你还在用C++11,但很羡慕这些新类型,你也可以自己动手实现:
| 1 | template <typename T> | 
Item10: 优先选用有界枚举来替代无界枚举
C++的通用规则是name会从属于它所在的scope,只在这个scope内可见。但C++98的enum没有遵循这个规则,enum内定义的name,不只在这个enum内可见,而是在enum所在的整个scope内可见!
| 1 | enum Color {black, white, red}; | 
这种enum我们称为“无界枚举”,源自C。C++11增加了一种有界的枚举,来解决name泄漏到整个scope的问题。
| 1 | enum class Color {black, white, red}; | 
enum class定义的枚举又被称为“枚举类”。而且它还有一个特性:无法隐式转换为整数类型(也因此无法隐式转换为浮点类型):
| 1 | enum Color {black, white, red}; | 
上面的转换显然不是我们预期的,而用enum class就可以避免这种转换:
| 1 | enum class Color {black, white, red}; | 
如果实在需要做这种转换,我们可以用static_cast来做,比如static_cast<std::size_t>(c)。
enum class还有一个特性:可以前向声明。C++98中的无界enum则不能前向声明。C++11中的无界enum可以在指定底层类型后前向声明。
| 1 | enum Color; // error! | 
为什么C++98只支持enum的定义,而不支持声明?因为C++中每个enum类都对应着一个底层整数类型,但C++98中我们没办法指定这个类型,也没办法确定这个类型。编译器会在看到enum的定义时确定它底层用什么整数类型来存储。而前向声明的一个基本要求是:知道对应类型的大小。如果我们没办法确定enum是用什么类型存储的,我们也就没办法知道enum的大小,也就没办法前向声明。
| 1 | enum Status { | 
编译器在看到上面的Status定义时,发现它的所有的值范围都在[-1, 200]之间,最合适的类型就是char。如果我们增加一项audited = 500,值范围就变成了[-1, 500],最合适的类型变成了short!
但C++98这种过于死板的规定也导致了:每当我们向Status中增加一项,所有引用了它的.cpp文件都需要被重新编译一次。
C++11允许我们指定enum的底层类型,尤其是enum class在不指定时默认使用int。这就保证了我们能安全的前向声明enum和enum class。
| 1 | enum class Status; // underlying type is int | 
注意:如果要指定底层类型,需要在enum和enum class的声明和定义处都指定相同的底层类型。
那么enum class有什么地方不如enum呢?还真有。
C++11中我们使用std::tuple时需要用到整数常量作为下标:
| 1 | auto val = std::get<1>(uInfo); | 
如果用enum来代替硬编码的下标,对可读性有好处:
| 1 | enum UserInfoFields {uiName, uiEmail, uiReputation}; | 
但换成enum class上面的代码就不行了:enum class没办法隐式转换为整数类型。但我们可以用static_cast。
| 1 | enum class UserInfoFields {uiName, uiEmail, uiReputation}; | 
这么写太长了,我们可能需要一个辅助函数。注意:std::get是模板,它的参数需要是编译期常量,因此这个辅助函数必须是一个constexpr函数。我们来通过std::underlying_type实现一个将enum class的值转换为它的底层类型值的constexpr函数:
| 1 | template <typename E> | 
C++14中我们可以用std::underlying_type_t:
| 1 | template <typename E> | 
这样我们在使用std::tuple时就可以:
| 1 | auto val = std::get<toUType(UserInfoFields::uiEmail)>(uInfo); | 
还是有点长,但已经好多了,尤其是考虑到enum class相比于enum的其它好处:不污染namespace;不会莫名的做隐式转换。
目录
- Chapter1 类型推断 (Item 1-4)
- Chapter2 auto (Item 5-6)
- Chapter3 现代C++(Item 7-10)
- Chapter3 现代C++(Item 11-14)
- Chapter3 现代C++(Item 15-17)
- Chapter4 智能指针 (Item 18-22)
- Chapter5 右值引用、移动语义、完美转发(Item 23-26)
- Chapter5 右值引用、移动语义、完美转发(Item 27-30)
- Chapter6: Lamba表达式 (Item 31-34)
- Chapter7: 并发API (Item 35-37)
- Chapter7: 并发API (Item 38-40)
- Chapter8: 杂项 (Item 41-42)