Look again at this Item's title:
Consider pass by value for copyable parameters that are cheap to move and always copied.
It's worded the way it is for a reason. Four reasons, in fact:
1. You should only consider using pass by value. Yes, it requires writing only one function. Yes, it generates only one function in the object code. Yes, it avoids the issues associated with universal references. But it has a higher cost than the alternatives, and, as we'll see below, in some cases, there are expenses we haven't yet discussed.
2. Consider pass by value only for copyable parameters . Parameters failing this test must have move-only types, because if they're not copyable, yet the function always makes a copy, the copy must be created via the move constructor. [23] Sentences like this are why it'd be nice to have terminology that distinguishes copies made via copy operations from copies made via move operations.
Recall that the advantage of pass by value over overloading is that with pass by value, only one function has to be written. But for move-only types, there is no need to provide an overload for lvalue arguments, because copying an lvalue entails calling the copy constructor, and the copy constructor for move-only types is disabled. That means that only rvalue arguments need to be supported, and in that case, the “overloading” solution requires only one overload: the one taking an rvalue reference.
Consider a class with a std::unique_ptr
data member and a setter for it. std::unique_ptr
is a move-only type, so the “overloading” approach to its setter consists of a single function:
class Widget {
public:
…
void setPtr( std::unique_ptr&&ptr)
{ p = std::move(ptr); }
private:
std::unique_ptr p;
};
A caller might use it this way:
Widget w;
…
w.setPtr( std::make_unique("Modern C++"));
Here the rvalue std::unique_ptr
returned from std::make_unique
(see Item 21) is passed by rvalue reference to setPtr
, where it's moved into the data member p
. The total cost is one move.
If setPtr
were to take its parameter by value,
class Widget {
public:
…
void setPtr( std::unique_ptrptr)
{ p = std::move(ptr); }
…
};
the same call would move construct the parameter ptr
, and ptr
would then be move assigned into the data member p
. The total cost would thus be two moves — twice that of the “overloading” approach.
3. Pass by value is worth considering only for parameters that are cheap to move . When moves are cheap, the cost of an extra one may be acceptable, but when they're not, performing an unnecessary move is analogous to performing an unnecessary copy, and the importance of avoiding unnecessary copy operations is what led to the C++98 rule about avoiding pass by value in the first place!
4. You should consider pass by value only for parameters that are always copied . To see why this is important, suppose that before copying its parameter into the names
container, addName
checks to see if the new name is too short or too long. If it is, the request to add the name is ignored. A pass-by-value implementation could be written like this:
class Widget {
public:
void addName(std::string newName) {
if ((newName.length() >= minLen) &&
(newName.length() <= maxLen)){
names.push_back(std::move(newName));
}
}
…
private:
std::vector names;
};
This function incurs the cost of constructing and destroying newName
, even if nothing is added to names
. That's a price the by-reference approaches wouldn't be asked to pay.
Even when you're dealing with a function performing an unconditional copy on a copyable type that's cheap to move, there are times when pass by value may not be appropriate. That's because a function can copy a parameter in two ways: via construction (i.e., copy construction or move construction) and via assignment (i.e., copy assignment or move assignment). addName
uses construction: its parameter newName
is passed to vector::push_back
, and inside that function, newName
is copy constructed into a new element created at the end of the std::vector
. For functions that use construction to copy their parameter, the analysis we saw earlier is complete: using pass by value incurs the cost of an extra move for both lvalue and rvalue arguments.
When a parameter is copied using assignment, the situation is more complicated. Suppose, for example, we have a class representing passwords. Because passwords can be changed, we provide a setter function, changeTo
. Using a pass-by-value strategy, we could implement Password
like this:
class Password {
public:
explicit Password( std::string pwd) // pass by value
: text(std::move(pwd)){} // construct text
void changeTo( std::string newPwd) // pass by value
{ text = std::move(newPwd);} // assign text
…
private:
std::string text; // text of password
};
Storing the password as plain text will whip your software security SWAT team into a frenzy, but ignore that and consider this code:
std::string initPwd("Supercalifragilisticexpialidocious");
Password p(initPwd);
There are no suprises here: p.text
is constructed with the given password, and using pass by value in the constructor incurs the cost of a std::string
move construction that would not be necessary if overloading or perfect forwarding were employed. All is well.
A user of this program may not be as sanguine about the password, however, because “Supercalifragilisticexpialidocious” is found in many dictionaries. He or she may therefore take actions that lead to code equivalent to the following being executed:
std::string newPassword = "Beware the Jabberwock";
p.changeTo(newPassword);
Whether the new password is better than the old one is debatable, but that's the user's problem. Ours is that changeTo
's use of assignment to copy the parameter newPwd
probably causes that function's pass-by-value strategy to explode in cost.
The argument passed to changeTo
is an lvalue ( newPassword
), so when the parameter newPwd
is constructed, it's the std::string
copy constructor that's called. That constructor allocates memory to hold the new password. newPwd
is then move-assigned to text
, which causes the memory already held by text
to be deallocated. There are thus two dynamic memory management actions within changeTo
: one to allocate memory for the new password, and one to deallocate the memory for the old password.
But in this case, the old password (“Supercalifragilisticexpialidocious”) is longer than the new one (“Beware the Jabberwock”), so there's no need to allocate or deallocate anything. If the overloading approach were used, it's likely that none would take place:
Читать дальше