每周技巧 #182:初始化你的整数!

本节阅读量:

本文翻译自 Abseil 官网的 Tip of the Week #182: Initialize Your Ints!

原文最初作为 TotW #182 发布于 2020 年 7 月 23 日。

更新于 2020 年 7 月 23 日。

快捷链接:abseil.io/tips/182

“在任何决策时刻,你能做的最好事情是正确的事,其次是错误的事,最糟糕的是无所作为。” – Theodore Roosevelt

C++ 太容易留下未初始化变量了。这很可怕,因为几乎任何对未初始化对象的访问都会导致未定义行为。在众多初始化形式中,默认初始化确实是“默认”的,当变量没有指定初始值时就会发生,但它并不总是意味着真正的初始化

平凡类型的默认初始化

1
2
3
4
{
  bool bool_one;
  bool bool_two = bool_one;
}

很多人会惊讶地发现,上面的代码片段触发了未定义行为。在第一条语句中,bool_one默认初始化,而这(讽刺的是)并不保证真的初始化变量。在这个例子中,尽管使用了“默认初始化”,bool_one 仍然未初始化。我们如何知道这一点?

为了理解这个现象,先澄清默认初始化何时会、何时不会这样表现。在 C++ 中,并非所有类型都暴露跳过初始化的能力。这里有两个主要类别值得强调。

  1. 对于有默认构造函数的类型,包括大多数 class 类型,默认初始化在所有情况下都会调用默认构造函数。例如,std::string str; 保证会初始化 str,就像它被值初始化std::string str{}; 一样。
  2. 对于没有构造函数的类型,比如 bool,默认初始化可能表现出两种行为。A)如果被初始化的变量是 static 或定义在命名空间作用域,会执行所谓的“值初始化”。B)但是,对于非 static 的块作用域变量,默认初始化对这些类型完全不执行初始化,让变量保持未初始化并具有不确定值。

因此,在上面的例子中,bool_one 未初始化,因为 bool 没有构造函数,且 bool_one 是非 static 的块作用域变量。当 bool_two 的初始化读取 bool_one 的值时,结果行为是未定义的。

C++ 中哪些类型缺少构造函数?

C++ 继承了 C 中的类型,它们被称为平凡默认可构造类型,口语中也叫“平凡”类型,其实现没有构造函数。这包括 intdouble 这样的基础类型,也包括只包含平凡字段且没有逐成员初始化器的 struct 类型。它还包括所有原始指针类型,即便它们指向的是类,例如 MyClass*

换句话说,由于 C 没有构造函数,这类类型在 C++ 中用于默认初始化时保留了这种行为。

为什么 C++ 允许未初始化对象?

少数情况下,保留某些对象未初始化的能力对性能有用,或者可用于提供确实没有初始值的占位符。由于大多数访问未初始化值的模式都是未定义的,sanitizers 也可以利用这些信息查找 bug。

可能平凡的类型的默认初始化

和前面的代码片段一样,下面的代码也使用了默认初始化

1
2
3
{
  MyType my_variable;
}

读取 my_variable 的值安全吗?

要回答这个问题,我们必须了解更多 MyType 的实现。所展示的调用点没有足够信息判断读取 my_variable 是否安全。例如,如果 MyType 是一个简单 struct 类型,只包含 int 字段,没有构造函数,也没有逐成员初始化器,那么 my_variable 将未初始化。然而,如果 MyType 是一个 class 类型,并且有用户定义的 MyType::MyType() 实现,那么构造函数负责初始化变量,使得立刻读取它的值是安全操作。

建议:初始化平凡对象

在大多数代码中,你大概不想要未初始化对象。少数情况下,出于性能或编码语义,这样做可能合理。不过,除非处于这些例外场景之一,否则请优先初始化 struct 字段和变量中的平凡对象,如下面例子所示:

1
2
3
4
5
float ComputeValueWithDefault() {
  float value = 0.0;  // 通过提供默认值来保证初始化。
  ComputeValue(&value);
  return value;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
struct MySequence {
  // 逐成员初始化器保证初始化。
  MyClass* first_element = nullptr;
  int element_count = 0;
};

MySequence GetPopulatedMySequence() {
  MySequence my_sequence;  // 因逐成员初始化器而安全。
  MaybePopulateMySequence(&my_sequence);
  return my_sequence;
}

此外,在可行时,避免为平凡类型创建类型别名。我们希望 structclass 类型在所有情况下都能安全初始化。由于基础类型(整数、指针等)在默认初始化时不保证初始化,给这些类型起一个看起来安全的名字,会让代码更难推理。

下面是平凡类型别名的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
  using KeyType = float;  // C++ 风格别名
  typedef bool ResultT;  // C 风格别名

  // [很多行代码...]

  // 惊喜!这些变量未初始化!
  KeyType some_key;
  ResultT some_result;
}

更多信息

每周技巧 #181:访问 `StatusOr<T>` 的值

上一节

每周技巧 #186:优先把函数放进未命名命名空间

下一节