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)