Tip of the Week #182: Initialize Your Ints!

Originally posted as TotW #182 on July 23, 2020

Updated 2020-07-23

Quicklink: abseil.io/tips/182

“In any moment of decision, the best thing you can do is the right thing, the next best thing is the wrong thing, and the worst thing you can do is nothing.” –Theodore Roosevelt

C++ makes it too easy to leave variables uninitialized. This is scary, because almost any access to an uninitialized object results in Undefined Behavior. Default initialization is the default, among many forms, and occurs when no initial value is specified for a variable, but it is not always initialization.

Default Initialization of Trivial Types

{
  bool bool_one;
  bool bool_two = bool_one;
}

It surprises many to learn that the above code snippet invokes Undefined Behavior. In the first statement, bool_one is default initialized, which (ironically) is not guaranteed to actually initialize the variable. In the example, bool_one is left uninitialized even though it uses “default initialization”. How do we know this?

To understand this phenomenon, let’s first clarify when default initialization does and does not behave in this way. In C++, not all types expose the ability to skip initialization. There are two primary categories that are worth highlighting.

1) For types that have default constructors, including most class types, default initialization will invoke the default constructor in all cases. For example, std::string str; is guaranteed to initialize str as if it had been value initialized as in std::string str{};.

2) For types with no constructors, such as bool, default initialization can exhibit one of two possible behaviors. A) If the variable being initialized is static or defined at namespace scope, so-called “value initialization” will be performed. B) However, for non-static, block-scope variables, default initialization performs no initialization at all for these types, leaving the variable uninitialized with an indeterminate value.

As a result, in the above example, bool_one is uninitialized because bool has no constructors and bool_one is a non-static, block-scope variable. When bool_two’s initialization reads the value of bool_one, the resulting behavior is undefined.

Which types in C++ lack constructors?

C++ inherits types from C, referred to as trivially default constructible (or colloquially “trivial”) types, which are implemented with no constructors. This includes fundamental types like int and double as well as struct types that contain only trivial fields with no member-wise initializers. It also includes all raw pointer types, even when they point to classes as in MyClass*.

Said another way, since C does not have constructors, such types when used in C++ retain that behavior for default initialization.

Why does C++ allow for uninitialized objects?

The ability to leave some objects uninitialized is needed on rare occasion for performance or for providing placeholders where there is truly no initial value. Since most access patterns of uninitialized values are undefined, sanitizers can also use this information to find bugs.

Default Initialization of Potentially-Trivial Types

Like the code snippet before, the following code also uses default initialization:

{
  MyType my_variable;
}

Is it safe to read the value of my_variable?

To answer that question, we must know more about the implementation of MyType. The callsite shown does not have enough information to determine whether reading my_variable is safe. For example, if MyType is a simple struct type with only int fields, no constructors and no member-wise initializers, my_variable will be uninitialized. However, if MyType is a class type with a user-defined implementation for MyType::MyType(), the constructor is responsible for initializing the variable such that immediately reading its value is a safe operation.

Suggestion: Initialize Trivial Objects

In most code, you probably don’t want uninitialized objects. Rarely, it can make sense for reasons of performance or encoding semantics. However, unless in one of these exceptional cases, prefer initializing trivial objects in struct fields and variables as in the following examples:

float ComputeValueWithDefault() {
  float value = 0.0;  // Guarantees initialization by providing a default value.
  ComputeValue(&value);
  return value;
}
struct MySequence {
  // Member-wise initializers guarantee initialization.
  MyClass* first_element = nullptr;
  int element_count = 0;
};

MySequence GetPopulatedMySequence() {
  MySequence my_sequence;  // Made safe by the member-wise initializers.
  MaybePopulateMySequence(&my_sequence);
  return my_sequence;
}

Additionally, where feasible, refrain from making type aliases for trivial types. We want struct and class types to be safely initialized in all cases. Since fundamental types (integers, pointers, etc.) do not guarantee initialization from default initialization, giving names to such types that appear to be safe can make code more difficult to reason about.

Examples of aliases for trivial types are shown below:

{
  using KeyType = float;  // C++-style alias
  typedef bool ResultT;  // C-style alias

  // [Many lines of code...]

  // Surprise! These variables are uninitialized!
  KeyType some_key;
  ResultT some_result;
}

Additional Information


Subscribe to the Abseil Blog