An alternative approach is to make addName
a function template taking a universal reference (see Item 24):
class Widget {
public:
template // take lvalues
void addName( T&& newName) // and rvalues;
{ // copy lvalues,
names.push_back(std::forward(newName)); // move rvalues;
} // see Item 25
// for use of
… // std::forward
};
This reduces the source code you have to deal with, but the use of universal references leads to other complications. As a template, addName
's implementation must typically be in a header file. It may yield several functions in object code, because it not only instantiates differently for lvalues and rvalues, it also instantiates differently for std::string
and types that are convertible to std::string
(see Item 25). At the same time, there are argument types that can't be passed by universal reference (see Item 30), and if clients pass improper argument types, compiler error messages can be intimidating (see Item 27).
Wouldn't it be nice if there were a way to write functions like addName
such that lvalues were copied, rvalues were moved, there was only one function to deal with (in both source and object code), and the idiosyncrasies of universal references were avoided? As it happens, there is. All you have to do is abandon one of the first rules you probably learned as a C++ programmer. That rule was to avoid passing objects of user-defined types by value. For parameters like newName
in functions like addName
, pass by value may be an entirely reasonable strategy.
Before we discuss why pass-by-value may be a good fit for newName
and addName
, let's see how it would be implemented:
class Widget {
public:
void addName( std::stringnewName) // take lvalue or
{ names.push_back(std::move(newName)); } // rvalue; move it
…
};
The only non-obvious part of this code is the application of std::move
to the parameter newName
. Typically, std::move
is used with rvalue references, but in this case, we know that (1) newName
is a completely independent object from whatever the caller passed in, so changing newName
won't affect callers and (2) this is the final use of newName
, so moving from it won't have any impact on the rest of the function.
The fact that there's only one addName
function explains how we avoid code duplication, both in the source code and the object code. We're not using a universal reference, so this approach doesn't lead to bloated header files, odd failure cases, or confounding error messages. But what about the efficiency of this design? We're passing by value . Isn't that expensive?
In C++98, it was a reasonable bet that it was. No matter what callers passed in, the parameter newName
would be created by copy construction . In C++11, however, addName
will be copy constructed only for lvalues. For rvalues, it will be move constructed . Here, look:
Widget w;
…
std::string name("Bart");
w.addName(name); // call addName with lvalue
…
w.addName(name + "Jenne"); // call addName with rvalue
// (see below)
In the first call to addName
(when name
is passed), the parameter newName
is initialized with an lvalue. newName
is thus copy constructed, just like it would be in C++98. In the second call, newName
is initialized with the std::string
object resulting from a call to operator+
for std::string
(i.e., the append operation). That object is an rvalue, and newName
is therefore move constructed.
Lvalues are thus copied, and rvalues are moved, just like we want. Neat, huh?
It is neat, but there are some caveats you need to keep in mind. Doing that will be easier if we recap the three versions of addName
we've considered:
class Widget { // Approach 1:
public: // overload for
void addName( const std::string&newName) // lvalues and
{ names.push_back(newName); } // rvalues
void addName( std::string&&newName)
{ names.push_back(std::move(newName)); }
…
private:
std::vector names;
};
class Widget { // Approach 2:
public: // use universal
template// reference
void addName( T&& newName)
{ names.push_back(std::forward(newName)); }
…
};
class Widget { // Approach 3:
public: // pass by value
void addName( std::stringnewName)
{ names.push_back(std::move(newName)); }
…
}
;
I refer to the first two versions as the “by-reference approaches,” because they're both based on passing their parameters by reference.
Here are the two calling scenarios we've examined:
Widget w;
…
std::string name("Bart");
w.addName(name); // pass lvalue
…
w.addName(name + "Jenne"); // pass rvalue
Now consider the cost, in terms of copy and move operations, of adding a name to a Widget
for the two calling scenarios and each of the three addName
implementations we've discussed. The accounting will largely ignore the possibility of compilers optimizing copy and move operations away, because such optimizations are context- and compiler-dependent and, in practice, don't change the essence of the analysis.
• Overloading: Regardless of whether an lvalue or an rvalue is passed, the caller's argument is bound to a reference called newName
. That costs nothing, in terms of copy and move operations. In the lvalue overload, newName
is copied into Widget::names
. In the rvalue overload, it's moved. Cost summary: one copy for lvalues, one move for rvalues.
• Using a universal reference: As with overloading, the caller's argument is bound to the reference newName
. This is a no-cost operation. Due to the use of std::forward
, lvalue std::string
arguments are copied into Widget::names
, while rvalue std::string
arguments are moved. The cost summary for std::string
arguments is the same as with overloading: one copy for lvalues, one move for rvalues.
Item 25explains that if a caller passes an argument of a type other than std::string
, it will be forwarded to a std::string
constructor, and that could cause as few as zero std::string
copy or move operations to be performed. Functions taking universal references can thus be uniquely efficient. However, that doesn't affect the analysis in this Item, so we'll keep things simple by assuming that callers always pass std::string
arguments.
• Passing by value: Regardless of whether an lvalue or an rvalue is passed, the parameter newName
must be constructed. If an lvalue is passed, this costs a copy construction. If an rvalue is passed, it costs a move construction. In the body of the function, newName
is unconditionally moved into Widget::names
. The cost summary is thus one copy plus one move for lvalues, and two moves for rvalues. Compared to the by-reference approaches, that's one extra move for both lvalues and rvalues.
Читать дальше