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 Declarations= delete
Originally posted as TotW #149 on May 3, 2018
Updated 2020-04-06
Quicklink: abseil.io/tips/149
Into the blue again after the money’s gone<br/> Once in a lifetime, water flowing underground –David Byrne
=delete
for LifetimesImagine you have an API that requires a reference to some long-lived object, but doesn’t take ownership of it.
class Request { ... // The provided Context must live as long as the current Request. void SetContext(const Context& context); };
You think to yourself, “Hey, what happens if someone passes a temporary? That’s going to be a bug. But this is modern C++, I can prevent that!” So you rig together a change in the API, adding a deleted overload.
class Request { ... // The provided Context must live as long as the current Request. void SetContext(const Context& context); void SetContext(Context&& context) = delete; };
Pleased with your work, you think “Hey, now the API says everything, the comment isn’t necessary.”
class Request { ... void SetContext(const Context& context); void SetContext(Context&& context) = delete; };
Was this a good idea? Why or why not?
As presented, you might think that this is a good idea. However, as in many cases of API design it is tempting to look at the definition of the API, but more useful to look at how the API is used. So lets replay this scenario, taking into account usage.
A user attempting to use the original SetContext()
, trying to get something
simple to build and not knowing where to find the right Context
object, just
makes the suggested call.
request.SetContext(Context());
Without your =delete
change, this builds, but fails at runtime (probably in a
mysterious fashion). When looking at the SetContext
API, the lifetime
requirement is documented, and the code is changed to comply.
request.SetContext(request2.context());
A user attempting to use the “improved” SetContext()
with your =delete
change and no comment, on the other hand, first encounters the build break:
error: call to deleted member function 'SetContext' request.SetContext(Context()); ~~~~~~~~^~~~~~~~~~ :4:8: note: candidate function has been explicitly deleted void SetContext(Context&& context) = delete;
The user then thinks “Well, I can’t pass a temporary”, but having no information about the actual requirement, what is the most likely fix?
Context context; request.SetContext(context);
Now, the crux of the matter: How likely is it that the scope of the new
automatic variable context
is the right lifetime for this call? If your answer
is anything less than 100%, the lifetime requirement comment is still necessary.
class Request { ... // The provided Context must live as long as the current Request. void SetContext(const Context& context); void SetContext(Context&& context) = delete; };
Deleting a member of an overload set in this fashion is at best a half-measure. Yes, you avoid one class of bugs, but you also complicate the API. Relying on such a design is a sure-fire way to get a false sense of security: the C++ type system is simply not capable of encoding the necessary details about lifespan requirements for parameters.
Since the type system can’t actually get this right, we recommend you not complicate things with half-measures. Keep it simple - don’t try to rely on this pattern to disallow temporaries, it doesn’t work well enough to help.
=delete
for “Optimization”Let’s flip the situation around: perhaps it isn’t that you want to prevent temporaries, maybe you want to prevent copies.
future<bool> DnaScan(Config c, const std::string& sequence) = delete; future<bool> DnaScan(Config c, std::string&& sequence);
How likely is it that a caller of your API will never need to keep their value? If you cannot be 100% sure that you know exactly how your API will be used, this is a recipe for annoying your users. Consider making copies and invoking such an API given normal (non-deleted) design:
Config c1 = GetConfig(); Config c2 = GetConfig(); std::string s = GetDna(); // Kick off scans for both configs. auto scan1 = DnaScan(c1, s); auto scan2 = DnaScan(c2, std::move(s));
Since we see that the second scan is the last use of s
, we can just
std::move
into the value-consuming call. With the “cleverly optimized”
version, the code gets more sloppy looking.
Config c1 = GetConfig(); Config c2 = GetConfig(); std::string s = GetDna(); std::string s2 = s; // Kick off scans for both configs. auto scan1 = DnaScan(c1, std::move(s)); auto scan2 = DnaScan(c2, std::move(s2));
APIs are provided as building blocks and abstractions - the ecosystem of APIs is a platform to be assembled together in new and surprising ways that are more than what the provider of any single API might predict. Believing that you know certainly that nobody should ever make a copy runs counter to that. Further, the problem of inefficiency and copying when moving would suffice is far broader than any single API, and likely better solved with some combination of profiling, training, code review, and static analysis.
In the rare case when you can know for sure that an API has to be used in a
particular fashion: you probably should encode that in the types in question.
Don’t operate on std::string
as your representation of a DNA sequence; write a
Dna
class and make it move-only with an explicit (easy to scan for) way to do
the expensive copy operation. Put another way: properties of types should be
expressed in those types, not in the APIs that operate on them.
As a side note: it’s possible to apply the same reasoning for ref-qualifiers on
destructive accessors. Consider a class like std::stringbuf
- in C++20 it
gained an accessor to consume the contained string, presented as an overload set
with the existing accessor:
const std::string& str() const &; std::string str() &&;
(See Tip #148 for more info on reference-qualified methods and
overload sets). Looking at existing usage of std::stringbuf
, nearly every use
has a single stringbuf
that is used to produce one string. Ignoring the legacy
code, would it not be best to enforce that, and provide only the “efficient”
ref-qualified destructive member?
Of course not, for similar reasons to the DnaScan
example above: you cannot
know certainly that nobody needs it, and it’s not unsafe to provide the const
overload. Use ref-qualifiers only as an overload set for optimization, or when
the ref-qualifiers are necessary to enforce semantic correctness.
It is tempting to try to use rvalue-references or reference qualifiers in
conjunction with =delete
to provide a more “user friendly” API, enforcing
lifetimes or preventing optimization problems. In practice, those are usually
bad temptations. Lifetime requirements are much more complicated than the C++
type system can express. API providers can rarely predict every future valid
usage of their API. Avoiding these types of =delete
tricks keeps things
simple.