Originally posted as TotW #177 on April 6, 2020
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
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
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
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
So it is possible that the question becomes “Which should we prefer:
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
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
T& - any modifications of such a member are modifying the
Consider the implementation of
std::vector<T>. There will almost certainly be
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.)
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
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
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
There is one useful-but-unusual design that may mandate
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.
constand reference data members when building value types or move-only types.