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()
Originally posted as TotW #146 on April 19, 2018
Updated 2020-04-06
Quicklink: abseil.io/tips/146
“The road to success is always under construction.” – Lily Tomlin
For safety and readability, you should assume scalar objects are not initialized to a reasonable value until they have been explicitly set to a value. Use of initializers can ensure that scalar values are initialized to safe values.
When objects are created, they may be initialized or uninitialized. Uninitialized objects are not safe to read, but understanding when the object is uninitialized is not trivial.
The first thing to understand is if the type under construction is scalar,
aggregate, or some other type. A scalar type can be thought of as a simple
type: an integral or floating point arithmetic object; a pointer; an enum; a
pointer-to-member; nullptr_t
. An aggregate type is an array or a class with
nothing virtual, no non-public fields or bases, and no constructor declarations.
Another factor affecting whether an instance has been initialized to a value
that is safe to read is whether it has an explicit initializer. That is, the
object name in the statement is followed by ()
, {}
, or = {}
.
As these rules are not intuitive, the easiest rule to remember to ensure an object is initialized is to provide an initializer. This is called value-initialization and is distinct from default-initialization, which is what the compiler will perform otherwise for scalar and aggregate types.
If a type is defined with a user-defined constructor, it is not an aggregate type, and initialization gets much simpler with both value- and default-initialization invoking the constructor:
struct Foo { Foo() : v() {} int v; std::string s; }; int main() { Foo default_foo; Foo value_foo = {}; ... }
The = {}
triggers value-initialization of value_foo
, which calls Foo
’s
default constructor. After, v
is safe to read, because the constructor’s
initializer list value-initializes it. In fact, as v
does not have a class
type, this is a special case of value-initialization called
zero-initialization and value_foo.v
will have the value 0
.
Similarly, while default_foo
is default-initialized, it calls the same
constructor, so default_foo.v
is also zero-initialized and is safe to read.
Note that Foo::s
has a user-provided constructor, so it is value-initialized
in either case, and safe to read.
It is possible for the user to declare a constructor while asking the compiler
to provide the definition via =default
. For example:
struct Foo { Foo() = default; // "User-declared", NOT "user-provided". int v; }; int main() { Foo default_foo; Foo value_foo = {}; }
In this case, Foo
defines a user-declared, but not user-provided,
constructor. While this type will not be an aggregate, members will be
initialized as if for an aggregate. This means that default_foo.v
will be
uninitialized, while value_foo.v
will be zero-initialized. Note that
“user-declared” only applies to a default constructor which is defaulted (=
default
) at its point of declaration. A defaulted out-of-line
implementation (Foo::Foo() = default
) is considered user-provided and will
behave equivalently to a definition of Foo::Foo() {}
.
struct Foo { Foo() {} int v; }; int main() { Foo foo = {}; }
In this case, although Foo
has a user-provided constructor, it fails to
initialize v
. In this case, v
is once more default-initialized, which means
its value is undetermined, and it is unsafe to read.
In general, it is a good idea to replace the initializer with an explicit initialization to a value, even if that value is 0, for the benefit of the reader. This is called direct-initialization, which is a more specific form of value-initialization.
struct Foo { Foo() : v(0) {} int v; };
A simpler solution than defining constructors for classes, while still avoiding the pitfalls of default- vs value-initialization, is to initialize members of classes at declaration, wherever possible:
struct Foo { int v = 0; };
This ensures that no matter how the instance of Foo
is constructed, v
will
be initialized to a determinate value.
Default member initialization also serves as documentation, especially in the case of booleans, or non-zero initial values, as to what is a safe initial value for the member.
The full set of rules for when scalar values are safe to read after initialization:
()
, {}
, or = {}
initializer.new int[10]()
.It’s easy to forget to add an explicit initializer to array declarations, but this can lead to particularly pernicious initialization issues.
int main() { int foo[3]; int bar[3] = {}; ... }
Every element of foo
is default-initialized, while every element of bar
will
be zero-initialized.
Pop quiz: Do these stylistically different declarations affect the behaviour of the code?
struct Foo { Foo() = default; int v; }; struct Bar { Bar(); int v; }; Bar::Bar() = default; int main() { Foo f = {}; Bar b = {}; ... }
Many developers would reasonably assume that this may affect code generation quality, but otherwise is a style preference. As you might have guessed, because I’m asking, this is not the case.
The reason goes back to the section above on
User-Declared vs User-Provided Constructors.
As the constructor for Foo
is defaulted on declaration, it is not
user-provided (but it is user-declared). This means that while Foo
is not an
aggregate type, f.v
is still zero-initialized. However, Bar
has a
user-provided constructor, albeit created by the compiler as a defaulted
constructor. As this constructor does not explicitly initialize Bar::v
, b.v
will be default-initialized and unsafe to read.
=
, ()
, and {}
=default