string_view
operator+
vs. StrCat()
absl::Status
std::bind
absl::optional
and std::unique_ptr
absl::StrFormat()
make_unique
and private
Constructors.bool
explicit
= delete
)switch
Statements Responsibly= delete
AbslHashValue
and Youcontains()
std::optional
parametersif
and switch
statements with initializersinline
Variablesstd::unique_ptr
Must Be MovedAbslStringify()
vector.at()
auto
for Variable DeclarationsOriginally 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.
{ 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.
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.
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; }