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 #177 on April 6, 2020
Updated 2020-04-06
Quicklink: abseil.io/tips/177
When implementing a type, decide on type design first. Prioritize API over implementation details. One common example of this is the tradeoff between assignability of a type vs. qualifiers for data members.
Imagine you are writing a City
class, and discussing how to represent its
member variables. You know that it is short-lived, representing the city as a
snapshot in time, so things like population, name, and mayor could conceivably
be const
- we aren’t going to use the same object in a given program for years
and years, so we don’t need to account for changes in population, new census
results, or elections.
Should we have members like this?
private: const std::string city_name_; const Person mayor_; const int64_t population_;
Why or why not?
The common suggestion for “Yes, make those const
” hinges on the idea “Well,
those values aren’t going to change for a given City
, so since everything that
can be const
should be const
, make them const
.” That will make it easier
for maintainers of the class to avoid accidentally modifying those fields.
This misses a critically important concern: what sort of type is City
? Is this
a value? Or a bundle of business logic? Is it expected to be copyable,
move-only, or non-copyable? The set of operations you can write efficiently for
City
(as a whole) may be impacted by the question of whether a single member
is made const
, and that is often a bad tradeoff.
Specifically, if your class has const
members, it cannot be assigned to
(whether by copy-assignment or move-assignment). The language understands this:
if your type has a const
member, copy-assignment and move-assignment operators
will not be synthesized. You can still copy (or move) construct such an
object, but you cannot change it in any way after construction (even “just” to
copy from another object of the same type). Even if you write your own
assignment operators, you’ll quickly find that you (obviously) can’t overwrite
these const
members.
So it is possible that the question becomes “Which should we prefer: const
members or assignment operations?” However, even that is misleading, because
both are answered by the one important question, “What sort of type is
City
?” If it is intended to be a value type, that specifies the API
(including assignment operations), and API trumps implementation concern in
general.
It is important for those API design decisions to take priority over implementation-detail choices: in the general case, there are more engineers affected by the API of a type than implementation of a type. That is, there are more users of a type than maintainers of that type, so priority should go to design choices that affect the user above the implementer. Even if you think the type will never be used by anyone outside of the team that is maintaining it, software engineering is about interface design and abstraction - we should be prioritizing good interfaces.
The same reasoning applies to storing references as data members. Even if we
know that the member must be non-null, it is still usually preferable to store
T*
for value types, because references are not rebindable. That is, we cannot
re-point a T&
- any modifications of such a member are modifying the
underlying T
.
Consider the implementation of std::vector<T>
. There will almost certainly be
a T* data
member in any std::vector
implementation, pointing to the
allocation. We know from the specification of std::vector
that such an
allocation must usually be valid (except possibly for empty vectors). An
implementation that always has an allocation could make that T&
, right? (Yes,
I’m ignoring arrays and offsets here.)
Clearly not. std::vector
is a value type, it is copyable and assignable. If
the allocation was stored with a reference-to-the-first-member instead of
pointer-to-the-first-member, we wouldn’t be able to move-assign the storage, and
it’s unclear how we’d update data
when resizing normally. Our clever way of
telling other maintainers “This value is non-null” would be getting in the way
of providing users the desired API. Hopefully it is clear that this is the wrong
tradeoff.
Of course, if your choices about type design suggest that City
(or whatever
type you are thinking about) should be non-copyable, that leaves far fewer
constraints on your implementation. It isn’t right or wrong for a class to
hold const
or reference members, it’s only a concern when those implementation
decisions are constraining or corrupting the interface presented by that class.
If you’ve already made a thoughtful and conscious decision that your type need
not be copyable, it’s very reasonable for you to make different choices about
how to represent the data members of the class. (But see Tip #116
for some more thoughts and pitfalls around argument lifetime and reference
storage).
There is one useful-but-unusual design that may mandate const
members:
intentionally immutable types. Instances of such a type are immutable after
construction: no mutating methods, no assignment operators. These are fairly
rare, but can sometimes be useful. In particular, such a type is inherently
thread-safe because there are no mutating operations. Objects of such a type
can be freely shared among threads with no concern about data races or
synchronization. However, in exchange these objects may have significant
run-time overhead stemming from the need to copy them constantly. The
immutability even prevents these objects from being efficiently moved.
It is almost always preferable to design your type to be mutable but still thread-compatible, rather than relying on thread-safety-via-immutability. Users of your type are usually in a better position to judge the benefits of mutability case-by-case. Don’t force them to work around unusual design choices without very strong evidence showing why your use case is unusual.
const
and reference data members when building value types or
move-only types.