类是 C++ 代码的基础单元, 我们自然会广泛的使用它. 本节主要列出在写一个类时的守则.
避免在构造函数中调用虚函数, 也不要在无法报出错误时进行可能失败的初始化.
定义:
在构造函数中可以进行各种初始化操作.
优点:
- 不需要考虑类是否已经被初始化.
- 经过构造函数完全初始化后的对象可以为
const
, 并容易被标准容器和算法使用.
缺点:
-
如果如果在构造函数内调用了虚函数, 这些调用不会分派到子类的实现. 即使你写的类现在不是一个子类, 将来在修改这个类时还是可能会导致不易察觉的问题.
-
如果不让程序崩溃 (总归不太好) 或者抛出异常 (我们禁止这么做) 的话, 构造函数难以报错.
-
如果构造函数失败了, 我们就有了一个初始化失败的类, 这个对象有可能进入不正常的状态, 需要用
bool isValid()
或者类似的机制来检测, 但很容易忘记. -
不能获得构造函数的地址, 由构造函数完成的工作是无法以简单的方式进行移交的. 比如移交给另一个线程.
结论:
构造函数不允许调用虚函数. 如果情况允许, 让程序崩溃是一个合适的处理构造函数错误的方式. 要么就考虑使用工厂函数或者使用 Init()
方法. Avoid Init() methods on objects with no other states that affect which public methods may be called (semi-constructed objects of this form are particularly hard to work with correctly).
不要定义隐式转换. 使对于转换运算符和单参构造函数使用 explicit
关键字.
定义:
隐式转换允许某种类型 (源类型) 的对象被用于需要另一种类型 (目标类型) 的位置, 如将 int
参数传递给具有 double
参数的函数.
除了语言定义的隐式转换之外, 使用者可以通过向源类型或目标类型的类定义中添加适当的成员来定义自己需要的隐式转换. 在源类型中定义隐式类型转换, 可以通过目的类型名的类型转换运算符实现 (如 operator bool()
). 在目标类型中定义隐式类型转换, 则通过以源类型作为其唯一参数 (或唯一无默认值的参数) 的构造函数实现.
explicit
关键字可以用于构造函数或 (在 C++11 引入) 类型转换运算符, 以保证只有当目的类型在调用点被显式写明时才能进行类型转换, 例如使用 cast. 这不仅作用于隐式类型转换, 还能作用于 C++11 的列表初始化语法:
class Foo {
explicit Foo(int x, double y);
...
};
void Func(Foo f);
Func({42, 3.14}); // Error
这一代码从技术上说并非隐式类型转换, 但是语言标准认为这是 explicit
.
优点:
- 通过避免显式地写出那种一目了然的类型名, 隐式类型转换可以让一个类型的可用性和表达性更强.
- 隐式类型转换可以简单地取代函数重载.
- 在初始化对象时, 列表初始化语法是一种简洁明了的写法.
缺点:
- 隐式转换会隐藏类型不匹配的错误. 有时, 目的类型并不符合用户的期望, 甚至用户根本没有意识到发生了类型转换.
- 隐式类型转换会让代码难以阅读, 尤其是在有函数重载的时候, 因为这时很难判断到底是哪个函数被调用.
- 单参数构造函数有可能会被无意地用作隐式类型转换.
- 如果单参数构造函数没有加上
explicit
关键字, 读者无法判断这一函数究竟是要作为隐式类型转换, 还是作者忘了加上explicit
标记. - 并没有明确的方法用来判断哪个类应该提供类型转换, 这会使得代码变得含糊不清.
- 如果目的类型是隐式指定的, 那么列表初始化会出现和隐式类型转换一样的问题, 尤其是在列表中只有一个元素的时候.
结论:
在类型定义中, 类型转换运算符和单参数构造函数都应当用 explicit
进行标记. 一个例外是, 拷贝和移动构造函数不应当被标记为 explicit
, 因为它们并不执行类型转换. 对于设计目的就是用于对其他类型进行透明包装的类来说, 隐式类型转换有时是必要且合适的. 这时应当联系项目组长并说明特殊情况.
不能用单个参数调用的构造函数通常应该省略 explicit
. 采用单个 std::initializer_list
参数的构造函数也应该省略 explicit
,以支持复制初始化(例如 MyType m = {1,2};
).