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/126 on 2016-12-12
By James Dennett ([email protected]) based on a mailing list post by Titus Winters ([email protected])
As a codebase expands it is increasingly difficult to know the details of everything you depend on. Requiring deep knowledge doesn’t scale: we have to rely on interfaces and contracts to know that code is correct, both when writing and when reviewing. In many cases the type system can provide those contracts in a common fashion. Consistent use of type system contracts makes for easier authoring and reviewing of code by identifying places where there are potentially risky allocations or ownership transfers for objects allocated on the heap.
While in C++ we can reduce the need for dynamic memory allocation by using plain
values, sometimes we need objects to outlive their scope. C++ code should prefer
smart pointers (most commonly std::unique_ptr
) instead of raw pointers when
dynamically allocating objects. This provides a consistent story around
allocation and ownership transfer, and leaves a clearer visual signal where
there’s code that needs closer inspection for ownership issues. The side effect
of matching how allocation works in the outside world post-C++14 and being
exception safe is just icing.
Two key tools for this are absl::make_unique()
(a C++11 implementation of
C++14’s std::make_unique()
, for leak-free dynamic allocation) and
absl::WrapUnique()
(for wrapping owned raw pointers into the corresponding
std::unique_ptr
types). They can be found in
absl/memory/memory.h.
new
?Why should code prefer smart pointers and these allocation functions over raw
pointers and new
?
When possible, ownership is best expressed in the type system. This allows
reviewers to verify correctness (absence of leaks and of double-deletes)
almost entirely by local inspection. (In code that is exceptionally
performance sensitive, this may be excused: while cheap, passing
std::unique_ptr
across function boundaries by value has non-zero overhead
because of ABI constraints. That’s rarely important enough to justify
avoiding it.)
Somewhat like the reasoning for preferring push_back()
over
emplace_back()
(TotW 112), absl::make_unique()
directly
expresses the intent and can only do one thing (do the allocation with a
public constructor, returning a std::unique_ptr
of the specified
type). There’s no type conversion or hidden behavior. absl::make_unique()
does what it says on the tin.
The same could be achieved with std::unique_ptr<T> my_t(new T(args));
but
that is redundant (repeating the type name T
) and for some people there’s
value in minimizing calls to new
. More on this in #5.
If all allocations are handled via absl::make_unique()
or factory calls,
that leaves absl::WrapUnique()
for the implementation of those factory
calls, for code interacting with legacy methods that don’t rely on
std::unique_ptr
for ownership transfer, and for rare cases that need to
dynamically allocate with aggregate initialization (absl::WrapUnique(new
MyStruct{3.141, "pi"})
). In code review it’s easy to spot the
absl::WrapUnique
calls and evaluate “does that expression look like an
ownership transfer?” Usually it’s obvious (for example, it’s some factory
function). When it’s not obvious, we need to check the function to be sure
that it’s actually a raw-pointer ownership transfer.
If we instead rely mostly on the constructors of std::unique_ptr
, we see
calls like:
std::unique_ptr<T> foo(Blah());
std::unique_ptr<T> bar(new T());
It takes only a moment’s inspection to see that the latter is safe (no leak,
no double-delete). The former? It depends: if Blah()
is returning a
std::unique_ptr
, it’s fine, though in that case it would be more obviously
safe if written as
std::unique_ptr<T> foo = Blah();
If Blah()
is returning an ownership-transferred raw pointer, that’s also
fine. If Blah()
is returning just some random pointer (no transfer), then
there’s a problem. Reliance on absl::make_unique()
and absl::WrapUnique()
(avoiding the constructors) provides an additional visual clue for the
places where we have to worry (calls to absl::WrapUnique()
, and only those).
By default, use absl::make_unique()
(or std::make_shared()
for the rare
cases where shared ownership is appropriate) for dynamic allocation. For
example, instead of: std::unique_ptr<T> bar(new T());
write auto bar
= absl::make_unique<T>();
and instead of bar.reset(new T());
write
bar = absl::make_unique<T>();
In a factory function that uses a non-public constructor, return a
std::unique_ptr<T>
and use absl::WrapUnique(new T(...))
in the
implementation.
When dynamically allocating an object that requires brace initialization
(typically a struct, an array, or a container), use absl::WrapUnique(new
T{...})
.
When calling a legacy API that accepts ownership via a T*
, either allocate
the object in advance with absl::make_unique
and call ptr.release()
in
the call, or use new
directly in the function argument.
When calling a legacy API that returns ownership via a T*
, immediately
construct a smart pointer with WrapUnique
(unless you’re immediately
passing the pointer to another legacy API that accepts ownership via a
T*
).
Prefer absl::make_unique()
over absl::WrapUnique()
, and prefer
absl::WrapUnique()
over raw new
.