Item 7: 辨别生成对象时()和{}的不同
Item 8: 多用 nullptr 代替 0 和 null
Item 9: 多用 alias declarations 而不是 typedefs
Item 10: 多用 scoped enums 而不是 unscoped enums
Item 11: 使用 deleted 函数而不是 private undefined 函数
Item 12: 将重写的函数声明为 override
本文所有内容参考于 《Effective Modern C++》(Scott Meyers)一书,仅供个人学习
Moving to Modern C++
Item 7:辨别生成对象时()和{}的不同
当一个类没有以 initializer_list 为参数的构造函数时,使用()和{}并无二致:
1 | class Widget { |
但若有了以 initializer_list 为参数的构造函数,使用{}会优先匹配对应的构造函数:
1 | class Widget { |
这里可以看到,true 和 10 等都被隐式类型转换成了 long double,可见
initializer_list 的构造函数的优先级非常高。
它的优先级甚至高于了移动构造函数:
1 | class Widget { |
待编辑:说实话,这里并不太懂,w4会直接使用operator()?
1 | class Widget { |
从上面这例可以看出,编译器非常倾向于使用 std::initializer_list
的构造函数,从而使得正确匹配参数的构造函数并不会被使用。另外还有个小知识点:{}初始器中,缩小范围的隐式类型转换会报错。
只有在无法进行类型转换的情况下,编译器才会考虑其他的构造函数:
1 | class Widget { |
那么,当初始化是使用空的 {} 会怎么样呢?是空的 initializer_list 还是调用默认构造函数?结论是调用默认构造函数。当然了,如果你想使用空的 (),那会变成调用一个对应名字的函数了:
1 | class Widget { |
如果真的想调用空的 initializer_list,需要两重括号:
1 | Widget w4({}); // calls std::initializer_list ctor |
对于模板编写者来说,若想自己定义对于 () 中参数的反应,可以像如下代码这样:
1 | template<typename T, // type of object to create |
这样,对于下面的代码,localObject1 会是一个由10个元素组成的 vector,而 localObject2 是由2个元素组成的 vector。
1 | doSomeWork<std::vector<int>>(10, 20); |
Things to Remember
- Braced initialization is the most widely usable initialization syntax, it prevents narrowing conversions, and it’s immune to C++’s most vexing parse.
- During constructor overload resolution, braced initializers are matched to std::initializer_list parameters if at all possible, even if other constructors offer seemingly better matches.
- An example of where the choice between parentheses and braces can make a significant difference is creating a std::vector<numeric type> with two arguments.
- Choosing between parentheses and braces for object creation inside templates can be challenging.
Item 8:多用 nullptr 代替 0 和 null
当 C++ 发现一个需要用指针的地方出现了0时,它通常会把0看作是一个空指针。然而,C++主要的原则还是将0看作是一个int。NULL 也类似,编译允许在一些情况下将 NULL 当成一个整数(如 long)。 在 C++98 中,使用0和 NULL 的重载可能会有一些意外:
1 | void f(int); // three overloads of f |
如果 NULL 的定义是 0L,那么编译的结果可能是模糊的,因为从 long 到 int
和从 0L 到 void* 是同样优先的。
所以,之所以推荐使用 nullptr,就是因为 nullptr 没有 int
类型,从而可以把它看成是一个可以代表所有类型的空指针。
1 | f(nullptr); // calls f(void*) overload |
在 cppreference 网站上找到个有意思的例子:
1 | template<class F, class A> |
这个例子有意思在哪呢?可以知道,如果单纯的调用函数 g:
1 | g(0); |
是不会出错的,0 和 NULL 都被转换成了空指针;然而模板中 0 和 NULL 都被推断为 int 类型,从而导致了错误。这种错误排查起来可是很令人头疼的。
Things to Remember
- Prefer nullptr to 0 and NULL.
- Avoid overloading on integral and pointer types.
Item 9:多用 alias declarations 而不是 typedefs
当定义较长且常用时,C++11 以前一般用 typedef 来简化代码:
1 | typedef std::unique_ptr<std::unordered_map<std::string, std::string>> UPtrMapSS; |
C++11 提供了 alias declarations:
1 | using UPtrMapSS = std::unique_ptr<std::unordered_map<std::string, std::string>>; |
他们俩提供了相同的作用。那么为什么还提倡使用 alias declarations 呢?首先 alias declarations 在涉及到函数指针时更直观:
1 | // FP is a synonym for a pointer to a function taking an int and |
其次, alias declarations 可以模板化,而 typedef 不能:
1 | template<typename T> // MyAllocList<T> |
1 | template<typename T> // MyAllocList<T>::type |
当使用 typedef 时,如果我们想用 MyAllocList 在模板里创建一个对象,需要使用 typename:
1 | template<typename T> |
在这里,MyAllocList<T>::type
是一个依赖于参数T的依赖类型(dependent type),而C++要求依赖类型必须使用
typename。
而若是使用 alias template,代码会明快很多:
1 | template<typename T> |
之所以会这样,是因为使用 alias template 的 MyAllocList
在 C++11 中,有个叫 type_traits 的标准库,提供了允许用户得到不同的参数类型:
1 | std::remove_const<T>::type // yields T from const T |
注意这里都使用了 ::type。没错,这是 C++11 的,之所以不用 alias template 是由于一些历史原因。在 C++14 中,alias template 的 type_traits 也被加上了:
1 | std::remove_const<T>::type // C++11: const T → T |
“The C++11 constructs remain valid in C++14, but I don’t know why you’d want to use them.”
当然了,如果你想自己实现 C++14 类似的 alias templates,可以像下面这样做:
1 | template <class T> |
Things to Remember
- typedefs don’t support templatization, but alias declarations do.
- Alias templates avoid the “::type” suffix and, in templates, the “typename” prefix often required to refer to typedefs.
- C++14 offers alias templates for all the C++11 type traits transformations.
Item 10:多用 scoped enums 而不是 unscoped enums
在 C++98 有 enum 类型,它的用法是这样的:
1 | enum Color { black, white, red }; |
然而这样使用会有隐患,比如若在这个定义的下面加上这样一句,就会出错:
1 | auto white = false; |
这是由于 enum 里面的变量是包括在整个和 Color 一样的参数空间里的,即 Color 在哪有效,这些 black 等就在哪有效。 这样的范围没有被限制的 enums 被称作 unscoped enums。 为了避免这种隐患,C++11 引入了 scoped enums:
1 | enum class Color { black, white, red }; // black, white, red |
Scoped enums 也被称为 enum classes。
除了作用范围以外,unscoped enums 和 scoped enums
还有类型转换上的差距:
1 | enum Color { black, white, red }; // unscoped enum |
在 unscoped enums 里面的 enumerators 是被隐式转换为整数类型的,所以上面的代码不会出错。但是 scoped enums 是没有这种隐式类型转换的:
1 | enum class Color { black, white, red }; // enum is now scoped |
若真的想用,得用显示类型转换:
1 | if (static_cast<double>(c) < 14.5) { // odd code, but |
在 C++98,是不允许 enum 提前声明(forward-declared)的,因为编译器要根据 enum 里面 enumerators 的数量来决定用什么类型来表示这个 enum,从而提高程序的空间使用效率/运行速度。在 C++11 中允许了 forward-declared,这是因为 C++11 默认了 enumerators 的类型。想自己修改也行:
1 | enum class Status; // underlying type is int |
unscoped enums 也不是一无是处,它 space leaking 的特点也具有一定的价值:
1 | using UserInfo = // type alias; see Item 9 |
注意上面的代码使用了 unscoped enums 的隐式类型转换。
如果在这里想用使用 scoped enums 也行:
1 | enum class UserInfoFields { uiName, uiEmail, uiReputation }; |
但这显得有点繁杂,那是不是可以用一个返回 size_t 的函数代替 static_cast 呢?这是一个 tricky 的地方:std::get< > 是一个模板,尖括号里面的值的类型需要在编译时就能知道,否则编译器不能将模板实例化,从而导致编译失败。因此我们需要一个 constexpr 函数模板,它可以返回任意类型的 enum:
1 | template<typename E> |
C++14 可以改成:
1 | template<typename E> // C++14 |
或用上 auto:
1 | template<typename E> // C++14 |
这样,获取 email 的信息可以这样用了:
1 | auto val = std::get<toUType(UserInfoFields::uiEmail)>(uInfo); |
Things to remember
- C++98-style enums are now known as unscoped enums.
- Enumerators of scoped enums are visible only within the enum. They convert to other types only with a cast.
- Both scoped and unscoped enums support specification of the underlying type. The default underlying type for scoped enums is int. Unscoped enums have no default underlying type.
- Scoped enums may always be forward-declared. Unscoped enums may be forward-declared only if their declaration specifies an underlying type.
Item 11:使用 deleted 函数而不是 private undefined 函数
如果你想避免其他开发者使用某些函数,一般来说不定义这个函数就行了(这不显然么)。然而其实并没那么显然,在定义一个类时,C++ 会自己定义一些函数(在 item 17(这里到时加个链接) 中有详细介绍)。比如流的拷贝问题。在C++ 标准库中有个 basic_ios 的模板类,所有的输出、输入流都继承自这个模板类。对流的拷贝的作用并不清楚,所以要避免用户对流进行拷贝。最简单的做法是做一个空的定义,下面的代码来自 C++98(包括注释):
1 | template <class charT, class traits = char_traits<charT> > |
但是这样的话成员函数或是友元都有权限使用 private 里面的函数。C++11 有更好的做法—— deleted functions:
1 | template <class charT, class traits = char_traits<charT> > |
使用 delete
会让你在编译前就知道对这些函数的调用是不合法的,而若是友元等访问未定义的
private 函数,会在编译时才能知道。
有个小细节,deleted functions 是定义在 public 里的,原因是 C++
会先检查函数的可访问性(accessibility),然后才检查函数的删除属性(deleted
status)。如果放在 private 里面,C++ 可能只会说函数在 private
不能访问,而不是说该函数不能使用。因此,将代码的 private-and-not-defined
成员改成 public deleted 成员有益于改进 error 信息。
另外,deleted functions 可以用在其他的非成员函数上,如我们定义一个函数:
1 | bool isLucky(int number); |
有这么一些对它的调用:
1 | if (isLucky('a')) … // is 'a' a lucky number? |
如果我们想保证函数的输入只有整数的,可以使用 deleted function 来避免使用类型转换的合法类型:
1 | bool isLucky(int number); // original function |
deleted functions 还能避免模板函数的不合适的实例化。比如我们有一个作用于内置指针的模板:
1 | template<typename T> |
在 C++ 中,有两类特殊的指针。一类是 void* 指针,因为它们不能被解引用或自增、自减;另一类是 char* 指针,因为它们指向的是 C 风格的字符串,而不是单个的 char。想避免使用这些指针类型的调用,可以用 deleted functions:
1 | template<> |
另外,使用 const void* 和 const char* 的调用也应该被避免:
1 | template<> |
如果还想彻底点,还有 const volatile void* 和 const volatile char* ,然而暂时不懂 volatile(划掉)。
如果是在类里使用模板,像下面的代码是不会编译的:
1 | class Widget { |
其原因是对模板的特殊规定只能在命名空间(namespace scope)里写,而不能在类空间(class scope)。下面的代码可以实现上面代码的效果:
1 | class Widget { |
总结一下,C++98 的 private-and-not-defined 成员实现的就是 C++11 中 deleted functions 实现的效果,但它在类外不能用,在类内不一定能用,就算能用了也要等到链接(link-time)时才能起作用。所以坚持使用 deleted functions 吧。
Things to Remember
- Prefer deleted functions to private undefined ones.
- Any function may be deleted, including non-member functions and template instantiations.
Item 12:将重写的函数声明为 override
虚函数是 C++ 的类的一个工具。你可以在基类(base class)中定义或声明虚函数,并在派生类(derived classed)中重写(override)这个虚函数。然而何时该调用 overrided 的虚函数并不显然。先看下面这个例子:
1 | class Base { |
上面这个例子,就是通过了一个基类的接口,调用了派生类的函数。
重写(Overring)的发生有以下几个必要要求:
- 基类函数必须是虚函数(virtual)
- 基类与派生类的该函数(下称基派函)同名(除了析构函数)
- 基派函参数一致
- 基派函const 性(constness)一致
- 基派函返回类型与 exception specifications 兼容(compatible)
C++11 还加了一点:
- 基派函引用限定符(reference qualifiers)一致
成员函数的引用限定符可以用来保证函数只被用于左值或右值,如:
1 | class Widget { |
由重写引起的错误并不会被编译器发现,因为它是往往合法的,只是没有按你的原目的进行。如下面的代码,就完全没有任何重写:
1 | class Base { |
C++11 给出了避免上面这种情况的方案:把要重写的函数声明为 override:
1 | class Derived: public Base { |
这样,上面的代码就会因为重写失败而产生错误。
在 C++98 中也有 override:
1 | class Warning { // potential legacy class from C++98 |
所以如果看见老代码用了 override 函数并不用改。
接下来补充一下引用限定符(reference
qualifiers)的内容。假设有个Widget:
1 | class Widget { |
这样,vals1 和 vals2 调用的是不同的 data() 函数。这就是 接受右值的函数定义 的作用。
Things to Remember
- Declare overriding functions override.
- Member function reference qualifiers make it possible to treat lvalue and rvalue objects (*this) differently.