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 #166 on August 28, 2019
Updated 2020-04-06
Quicklink: abseil.io/tips/166
“Entia non sunt multiplicanda praeter necessitatem.” (“Entities should not be multiplied without necessity”) – William of Ockham
“If you don’t know where you’re going, you’re probably going wrong.” – Terry Pratchett
Starting in C++17, objects are created “in place” when possible.
class BigExpensiveThing { public: static BigExpensiveThing Make() { // ... return BigExpensiveThing(); } // ... private: BigExpensiveThing(); std::array<OtherThing, 12345> data_; }; BigExpensiveThing MakeAThing() { return BigExpensiveThing::Make(); } void UseTheThing() { BigExpensiveThing thing = MakeAThing(); // ... }
How many times does this copy or move a BigExpensiveThing
?
Prior to C++17, the answer was up to three: one for each return
statement, and one more when initializing thing
. This makes some sense: each
function potentially puts the BigExpensiveThing
in a different place, so a
move may be needed in order to put the value where the ultimate caller wants it.
In practice, however, the object was always constructed “in place” in the
variable thing
, with no moves being performed, and the C++ language rules
permitted these move operations to be “elided” to facilitate this optimization.
Since C++17, this code is guaranteed to perform zero copies or moves. In fact,
the above code is valid even if BigExpensiveThing
is not moveable. The
constructor call in BigExpensiveThing::Make
directly constructs the local
variable thing
in UseTheThing
.
So what’s going on?
When the compiler sees an expression like BigExpensiveThing()
, it does not
immediately create a temporary object. Instead, it treats that expression as
instructions for how to initialize some eventual object, but defers creating
(formally, “materializing”) a temporary object for as long as possible.
Generally, creation of an object is deferred until the object is given a name.
The named object (thing
in the above example) is directly initialized using
the instructions found by evaluating the initializer. If the name is a
reference, a temporary object will be materialized to hold the value.
As a consequence, objects are constructed directly in the right place, instead of being constructed somewhere else and then copied. This behavior is sometimes referred to as “guaranteed copy elision”, but that’s inaccurate: there was never a copy in the first place.
All you need to know is: objects are not copied until after they are first given a name. There is no extra cost in returning by value.
(Even after being given a name, local variables might still not be copied when returned from a function, due to the Named Return Value Optimization. See Tip 11 for details.)
There are two corner cases where a use of an object with no name results in a copy anyway:
class DerivedThing : public BigExpensiveThing { public: DerivedThing() : BigExpensiveThing(MakeAThing()) {} // might copy data_ };
struct Strange { int n; int *p = &n; }; void f(Strange s) { CHECK(s.p == &s.n); // might fail } void g() { f(Strange{0}); }
There are two flavors of expression in C++:
1
, or MakeAThing()
– expressions
that you might consider to have a non-reference type.s
or
thing.data_[5]
– expressions that you might consider to have a reference
type.This division is called the “value category”; the former are prvalues and the latter are glvalues. When we talked about objects without a name above, what we were really referring to was prvalue expressions.
All prvalue expressions are evaluated in some context that determines where they put their value, and the execution of the prvalue expression initializes that location with its value.
For example, in
BigExpensiveThing thing = MakeAThing();
the prvalue expression MakeAThing()
is evaluated as the initializer of the
thing
variable, so MakeAThing()
will directly initialize thing
. The
constructor passes a pointer to thing
into MakeAThing()
, and the return
statement in MakeAThing()
initializes whatever the pointer points to.
Similarly, in
return BigExpensiveThing();
the compiler has a pointer to an object to initialize, and initializes that
object directly by calling the BigExpensiveThing
constructor.