Widget makeWidget() // as before
{
Widget w;
…
return w;
}
compilers must either elide the copying of w
or they must treat the function as if it were written like this:
Widget makeWidget() {
Widget w;
…
return std::move(w); // treat w as rvalue, because
} // no copy elision was performed
The situation is similar for by-value function parameters. They're not eligible for copy elision with respect to their function's return value, but compilers must treat them as rvalues if they're returned. As a result, if your source code looks like this,
WidgetmakeWidget( Widget w) // by-value parameter of same
{ // type as function's return
…
return w;
}
compilers must treat it as if it had been written this way:
Widget makeWidget(Widget w) {
…
return std::move(w); // treat w as rvalue
}
This means that if you use std::move
on a local object being returned from a function that's returning by value, you can't help your compilers (they have to treat the local object as an rvalue if they don't perform copy elision), but you can certainly hinder them (by precluding the RVO). There are situations where applying std::move
to a local variable can be a reasonable thing to do (i.e., when you're passing it to a function and you know you won't be using the variable any longer), but as part of a return
statement that would otherwise qualify for the RVO or that returns a by-value parameter isn't among them.
Things to Remember
• Apply std::move
to rvalue references and std::forward
to universal references the last time each is used.
• Do the same thing for rvalue references and universal references being returned from functions that return by value.
• Never apply std::move
or std::forward
to local objects if they would otherwise be eligible for the return value optimization.
Item 26: Avoid overloading on universal references.
Suppose you need to write a function that takes a name as a parameter, logs the current date and time, then adds the name to a global data structure. You might come up with a function that looks something like this:
std::multiset names; // global data structure
void logAndAdd(const std::string& name) {
auto now = // get current time
std::chrono::system_clock::now();
log(now, "logAndAdd"); // make log entry
names.emplace(name); // add name to global data
} // structure; see Item 42
// for info on emplace
This isn't unreasonable code, but it's not as efficient as it could be. Consider three potential calls:
std::string petName("Darla");
logAndAdd( petName); // pass lvalue std::string
logAndAdd( std::string("Persephone")); // pass rvalue std::string
logAndAdd( "Patty Dog"); // pass string literal
In the first call, logAndAdd
's parameter name
is bound to the variable petName
. Within logAndAdd
, name
is ultimately passed to names.emplace
. Because name
is an lvalue, it is copied into names
. There's no way to avoid that copy, because an lvalue ( petName
) was passed into logAndAdd
.
In the second call, the parameter name
is bound to an rvalue (the temporary std::string
explicitly created from "Persephone"
). name
itself is an lvalue, so it's copied into names
, but we recognize that, in principle, its value could be moved into names
. In this call, we pay for a copy, but we should be able to get by with only a move.
In the third call, the parameter name
is again bound to an rvalue, but this time it's to a temporary std::string
that's implicitly created from "Patty Dog"
. As in the second call, name is copied into names
, but in this case, the argument originally passed to logAndAdd
was a string literal. Had that string literal been passed directly to emplace
, there would have been no need to create a temporary std::string
at all. Instead, emplace
would have used the string literal to create the std::string
object directly inside the std::multiset
. In this third call, then, we're paying to copy a std::string
, yet there's really no reason to pay even for a move, much less a copy.
We can eliminate the inefficiencies in the second and third calls by rewriting logAndAdd
to take a universal reference (see Item 24) and, in accord with Item 25, std::forward
ing this reference to emplace
. The results speak for themselves:
template
void logAndAdd( T&&name) {
auto now = std::chrono::system_clock::now();
log(now, "logAndAdd");
names.emplace( std::forward(name ));
}
std::string petName("Darla"); // as before
logAndAdd( petName); // as before, copy
// lvalue into multiset
logAndAdd( std::string("Persephone")); // move rvalue instead
// of copying it
logAndAdd( "Patty Dog"); // create std::string
// in multiset instead
// of copying a temporary
// std::string
Hurray, optimal efficiency!
Were this the end of the story, we could stop here and proudly retire, but I haven't told you that clients don't always have direct access to the names that logAndAdd
requires. Some clients have only an index that logAndAdd
uses to look up the corresponding name in a table. To support such clients, logAndAdd
is overloaded:
std::string nameFromIdx(int idx); // return name
// corresponding to idx
void logAndAdd( int idx) // new overload
{
auto now = std::chrono::system_clock::now();
log(now, "logAndAdd");
names.emplace( nameFromIdx(idx));
}
Resolution of calls to the two overloads works as expected:
std::string petName("Darla"); // as before
logAndAdd(petName); // as before, these
logAndAdd(std::string("Persephone")); // calls all invoke
logAndAdd("Patty Dog"); // the T&& overload
logAndAdd(22); // calls int overload
Actually, resolution works as expected only if you don't expect too much. Suppose a client has a short
holding an index and passes that to logAndAdd
:
short nameIdx;
… // give nameldx a value
logAndAdd(nameldx); // error!
The comment on the last line isn't terribly illuminating, so let me explain what happens here.
Читать дальше