class Password {
public:
…
void changeTo( const std::string& newPwd) // the overload
{ // for lvalues
text = newPwd;// can reuse text's memory if
// text.capacity() >= newPwd.size()
}
…
private:
std::string text; // as above
};
In this scenario, the cost of pass by value includes an extra memory allocation and deallocation — costs that are likely to exceed that of a std::string
move operation by orders of magnitude.
Interestingly, if the old password were shorter than the new one, it would typically be impossible to avoid an allocation-deallocation pair during the assignment, and in that case, pass by value would run at about the same speed as pass by reference. The cost of assignment-based parameter copying can thus depend on the values of the objects participating in the assignment! This kind of analysis applies to any parameter type that holds values in dynamically allocated memory. Not all types qualify, but many — including std::string
and std::vector
— do.
This potential cost increase generally applies only when lvalue arguments are passed, because the need to perform memory allocation and deallocation typically occurs only when true copy operations (i.e., not moves) are performed. For rvalue arguments, moves almost always suffice.
The upshot is that the extra cost of pass by value for functions that copy a parameter using assignment depends on the type being passed, the ratio of lvalue to rvalue arguments, whether the type uses dynamically allocated memory, and, if so, the implementation of that type's assignment operators and the likelihood that the memory associated with the assignment target is at least as large as the memory associated with the assignment source. For std::string
, it also depends on whether the implementation uses the small string optimization (SSO — see Item 29) and, if so, whether the values being assigned fit in the SSO buffer.
So, as I said, when parameters are copied via assignment, analyzing the cost of pass by value is complicated. Usually, the most practical approach is to adopt a “guilty until proven innocent” policy, whereby you use overloading or universal references instead of pass by value unless it's been demonstrated that pass by value yields acceptably efficient code for the parameter type you need.
Now, for software that must be as fast as possible, pass by value may not be a viable strategy, because avoiding even cheap moves can be important. Moreover, it's not always clear how many moves will take place. In the Widget::addName
example, pass by value incurs only a single extra move operation, but suppose that Widget::addName
called Widget::validateName
, and this function also passed by value. (Presumably it has a reason for always copying its parameter, e.g., to store it in a data structure of all values it validates.) And suppose that validateName
called a third function that also passed by value…
You can see where this is headed. When there are chains of function calls, each of which employs pass by value because “it costs only one inexpensive move,” the cost for the entire chain of calls may not be something you can tolerate. Using by-reference parameter passing, chains of calls don't incur this kind of accumulated overhead.
An issue unrelated to performance, but still worth keeping in mind, is that pass by value, unlike pass by reference, is susceptible to the slicing problem . This is well-trod C++98 ground, so I won't dwell on it, but if you have a function that is designed to accept a parameter of a base class type or any type derived from it , you don't want to declare a pass-by-value parameter of that type, because you'll “slice off” the derived-class characteristics of any derived type object that may be passed in:
class Widget { … }; // base class
class SpecialWidget: public Widget { … }; // derived class
void processWidget( Widgetw); // func for any kind of Widget,
// including derived types;
… // suffers from slicing problem
SpecialWidget sw;
…
processWidget(sw); // processWidget sees a
// Widget, not a SpecialWidget!
If you're not familiar with the slicing problem, search engines and the Internet are your friends; there's lots of information available. You'll find that the existence of the slicing problem is another reason (on top of the efficiency hit) why pass by value has a shady reputation in C++98. There are good reasons why one of the first things you probably learned about C++ programming was to avoid passing objects of user-defined types by value.
C++11 doesn't fundamentally change the C++98 wisdom regarding pass by value. In general, pass by value still entails a performance hit you'd prefer to avoid, and pass by value can still lead to the slicing problem. What's new in C++11 is the distinction between lvalue and rvalue arguments. Implementing functions that take advantage of move semantics for rvalues of copyable types requires either overloading or using universal references, both of which have drawbacks. For the special case of copyable, cheap-to-move types passed to functions that always copy them and where slicing is not a concern, pass by value can offer an easy-to-implement alternative that's nearly as efficient as its pass-by-reference competitors, but avoids their disadvantages.
Things to Remember
• For copyable, cheap-to-move parameters that are always copied, pass by value may be nearly as efficient as pass by reference, it's easier to implement, and it can generate less object code.
• Copying parameters via construction may be significantly more expensive than copying them via assignment.
• Pass by value is subject to the slicing problem, so it's typically inappropriate for base class parameter types.
Item 42: Consider emplacement instead of insertion.
If you have a container holding, say, std::string
s, it seems logical that when you add a new element via an insertion function (i.e., insert
, push_front
, push_back
, or, for std::forward_list
, insert_after
), the type of element you'll pass to the function will be std::string
. After all, that's what the container has in it.
Logical though this may be, it's not always true. Consider this code:
std::vector vs; // container of std::string
vs.push_back( "xyzzy"); // add string literal
Here, the container holds std::string
s, but what you have in hand — what you're actually trying to push_back
— is a string literal, i.e., a sequence of characters inside quotes. A string literal is not a std::string
, and that means that the argument you're passing to push_back
is not of the type held by the container.
push_back
for std::vector
is overloaded for lvalues and rvalues as follows:
template
class Allocator = allocator> // Standard
class vector {
public:
…
void push_back(const T& x); // insert lvalue
void push_back(T&& x); // insert rvalue
…
};
In the call
vs.push_back("xyzzy");
Читать дальше