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 #176 on March 12, 2020
Updated 2020-04-06
Quicklink: abseil.io/tips/176
Consider the following:
// Extracts the foo spec and the bar spec from the provided doodad. // Returns false if the input is invalid. bool ExtractSpecs(Doodad doodad, FooSpec* foo_spec, BarSpec* bar_spec);
Using (or implementing) this function correctly requires the developer to ask themselves a surprising number of questions:
foo_spec
and bar_spec
out or in/out parameters?foo_spec
or bar_spec
? Is it
appended to? Is it overwritten? Does it make the function CHECK-fail? Does
it make it return false
? Is it undefined behavior?foo_spec
be null? Can bar_spec
? If they cannot, does a null
pointer make the function CHECK-fail? Does it make it return false
? Is it
undefined behavior?foo_spec
and bar_spec
? In
other words, do they need to outlive the function call?false
is returned, what happens to foo_spec
and bar_spec
? Are
they guaranteed to be unchanged? Are they “reset” in some way? Is it
unspecified?One cannot answer any of these questions from the function signature alone, and
cannot rely on the C++ compiler to enforce these contracts. Function comments
can help, but often don’t. This function’s documentation, for example, is silent
on most of these issues, and is also ambiguous about what “input” means. Does it
refer only to doodad
, or to the other parameters too?
Furthermore, this approach inflicts boilerplate on every callsite: the caller
has to allocate FooSpec
and BarSpec
objects in advance in order to call the
function.
In this case, there’s a simple way to eliminate the boilerplate and encode the contracts in a way that the compiler can enforce.
Here’s how to make all these questions moot:
struct ExtractSpecsResult { FooSpec foo_spec; BarSpec bar_spec; }; // Extracts the foo spec and the bar spec from the provided doodad. // Returns nullopt if the input is invalid. std::optional<ExtractSpecsResult> ExtractSpecs(Doodad doodad);
This new API is semantically the same, but it is now much harder to misuse:
foo_spec
and bar_spec
because they are created from scratch by the function.foo_spec
and bar_spec
in
case of failure because they cannot even be accessed if nullopt
is
returned.This in turn reduces the likelihood of bugs and reduces cognitive load on the developer.
There are other benefits, too. For example the function is more easily
composable; that is, it can easily be used as part of a wider expression, e.g.
SomeFunction(ExtractSpecs(...))
.
FooSpec
and BarSpec
are protos, the out-parameter
approach lets the caller allocate those protos on a particular arena if
they wish. In the return-values approach, the arena would need to be
specified as an additional parameter or the callee would have to know it
already.std::optional
to represent a missing return
value. Consider returning std::variant
if you need a more flexible
representation with multiple alternatives.std::pair
or std::tuple
for this purpose.