(原文链接:https://abseil.io/tips/146 译者:clangpp@gmail.com)
每周贴士 #146: 默认与值初始化
- 最初发布于:2018-04-19
- 作者:Dominic Hamon
- 更新于:2020-04-06
- 短链接:abseil.io/tips/146
“通往成功的路总是在施工”——莉莉·汤普琳
长话短说(TL;DR)
为安全性和可读性起见,你应该假设标量对象(scalar objects)在被显式赋值之前都没有初始化为合理值。使用初始化器可以确保标量值被初始化成了安全的值。
介绍
对象被创建的时候,也许初始化了,也许没初始化。没初始化的对象不能被安全地读取,但要理解对象什么时候没被初始化可没那么容易。
第一件需要了解的事,是被构造的类型是标量(scalar)、聚合(aggregate)还是其他类型。标量 类型可以被想象成简单类型:整数或浮点数对象;指针;枚举;成员指针;nullptr_t
。聚合 类型是数组或简单类(只有公有、非静态数据成员,没有自定义构造函数,没有基类,没有虚函数,没有默认成员初始化器的类)。
为判断一个实例是否被初始化为可安全读取的值,还有一个因素是,它有没有显式的 初始化器。也就是说,语句里的对象名后面,有没有跟着()
、{}
或= {}
。
因为这些规则并不直观,所以确保对象被初始化的最简单规则是,提供一个初始化器。这被叫做 值初始化(value-initialization),区别于 默认初始化(default-initialization),后者是编译器为标量和聚合类型执行的操作。
自定义构造函数
如果类型的定义中有自定义构造函数,那它就不是聚合类型,初始化过程就非常简单了:值初始化和默认初始化都调用该构造函数:
struct Foo {
Foo() : v() {}
int v;
std::string s;
};
int main() {
Foo default_foo;
Foo value_foo = {};
...
}
= {}
触发了value_foo
的值初始化,调用了Foo
的默认构造函数。然后,v
就可以被安全地读取了,因为构造函数初始化列表以值初始化了它。事实上,因为v
不是类类型,所以这是值初始化的一个特例,被称为 零值初始化(zero-initialization),value_foo.v
的值为0
。
类似地,因为default_foo
是默认初始化的,它调用了同样的构造函数,所以default_foo.v
也被零值初始化,可以被安全地读取。
注意Foo::s
有自定义构造函数,所以它在两种情况下都是被值初始化的,可以被安全地读取。
自定义构造函数中未初始化的成员
struct Foo {
Foo() {}
int v;
};
int main() {
Foo foo = {};
}
这种情况下,虽然Foo
有自定义构造函数,但它没有初始化v
。这时候,v
又被默认初始化了,也就是说其值是不确定的,不能被安全地读取。
显式值初始化
一般情况下,为了读者考虑,最好将初始化器替换为显式地初始化为一个值,即使那个值是0。这被称作 直接初始化(direct-initialization),是值初始化的特殊形式。
struct Foo {
Foo() : v(0) {}
int v;
};
默认成员初始化
比为类定义构造函数更简单——且仍然绕过默认和值初始化的陷阱——的方案是,尽可能地在成员声明的时候初始化:
struct Foo {
int v = 0;
};
这保证了无论Foo
的实例如何构造,v
都将被初始化为一个确定的值。
默认成员初始化 还充当了文档,尤其是对布尔类型或非零的初始值,说明了该成员安全的初始值。
专业建议:标量零值初始化
标量类型初始化后可以被安全读取的完整规则:
- 类型后跟着显式地
()
、{}
或= {}
的初始化器。 - 构造中的类型的实例是数组中的一个元素,且后面跟着如前所述的初始化器。例如,
new int[10]()
。 - 构造中的类型的实例是一个类成员,该类的默认构造函数被禁用(译者注:有自定义构造函数),且外层对象(译者注:该类的实例)是以值初始化的。
- 构造中的类型的实例是静态的(static)或线程局部的(thread-local)。
- 构造中的类型的实例是一个类成员,该成员是聚合类型且有初始化器。
数组类型
人们很容易忘记给数组声明加一个显式的初始化器,但这会带来相当要命的初始化问题。
int main() {
int foo[3];
int bar[3] = {};
...
}
foo
的每个元素都是默认初始化的(译者注:对于int
类型意味着没有初始化,随机值),然而bar
的每个元素都会被零值初始化。
跑个题:辨别默认初始化的不同声明
热门小测:这两个风格不同的声明是否影响代码的行为?
struct Foo {
Foo() = default;
int v;
};
struct Bar {
Bar();
int v;
};
Bar::Bar() = default;
int main() {
Foo f = {};
Bar b = {};
...
}
很多开发者会合理地假设这可能会影响到生成代码的质量,但除此之外只是个风格偏好。你也许会猜到,因为我问了,所以这肯定不对。
原因要追溯到上面第一节说到的自定义构造函数。因为Foo
的构造函数是默认声明的,所以它不是自定义构造函数。这意味着Foo
是一个聚合类型,而f.v
是零值初始化的。然而,Bar
有自定义构造函数,虽然被编译器实现为默认构造函数。因为该构造函数没有显式地初始化Bar::v
,b.v
将被默认构造,不能被安全读取。
建议
- 显式地指定标量类型的初始值,而不是依赖零值初始化。
- 在显式地初始化或赋值给标量类型的实例之前,假设它们有不确定的值。
- 如果一个成员有敏感的默认值,且这个类有多个构造函数,请使用默认成员初始化器来确保其不会被忘了初始化。注意构造函数中的成员初始化器会覆盖默认值。
延伸阅读
- Tip #61: Default Member Initializers
- Tip #88: Initialization:
=
,()
, and{}
- Tip #131: Special member functions and
=default
- C++参考——初始化