// linear time . All elements in
// aw1 are moved into aw2
auto aw2 = std::move(aw1);
Note that the elements in aw1
are moved into aw2
. Assuming that Widget
is a type where moving is faster than copying, moving a std::array
of Widget
will be faster than copying the same std::array
. So std::array
certainly offers move support. Yet both moving and copying a std::array
have linear-time computational complexity, because each element in the container must be copied or moved. This is far from the “moving a container is now as cheap as assigning a couple of pointers” claim that one sometimes hears.
On the other hand, std::string
offers constant-time moves and linear-time copies. That makes it sound like moving is faster than copying, but that may not be the case. Many string implementations employ the small string optimization (SSO). With the SSO, “small” strings (e.g., those with a capacity of no more than 15 characters) are stored in a buffer within the std::string
object; no heap-allocated storage is used. Moving small strings using an SSO-based implementation is no faster than copying them, because the copy-only-a-pointer trick that generally underlies the performance advantage of moves over copies isn't applicable.
The motivation for the SSO is extensive evidence that short strings are the norm for many applications. Using an internal buffer to store the contents of such strings eliminates the need to dynamically allocate memory for them, and that's typically an efficiency win. An implication of the win, however, is that moves are no faster than copies, though one could just as well take a glass-half-full approach and say that for such strings, copying is no slower than moving.
Even for types supporting speedy move operations, some seemingly sure-fire move situations can end up making copies. Item 14explains that some container operations in the Standard Library offer the strong exception safety guarantee and that to ensure that legacy C++98 code dependent on that guarantee isn't broken when upgrading to C++11, the underlying copy operations may be replaced with move operations only if the move operations are known to not throw. A consequence is that even if a type offers move operations that are more efficient than the corresponding copy operations, and even if, at a particular point in the code, a move operation would generally be appropriate (e.g., if the source object is an rvalue), compilers might still be forced to invoke a copy operation because the corresponding move operation isn't declared noexcept
.
There are thus several scenarios in which C++11's move semantics do you no good:
• No move operations:The object to be moved from fails to offer move operations. The move request therefore becomes a copy request.
• Move not faster:The object to be moved from has move operations that are no faster than its copy operations.
• Move not usable:The context in which the moving would take place requires a move operation that emits no exceptions, but that operation isn't declared noexcept
.
It's worth mentioning, too, another scenario where move semantics offers no efficiency gain:
• Source object is lvalue:With very few exceptions (see e.g., Item 25) only rvalues may be used as the source of a move operation.
But the title of this Item is to assume that move operations are not present, not cheap, and not used. This is typically the case in generic code, e.g., when writing templates, because you don't know all the types you're working with. In such circumstances, you must be as conservative about copying objects as you were in C++98 — before move semantics existed. This is also the case for “unstable” code, i.e., code where the characteristics of the types being used are subject to relatively frequent modification.
Often, however, you know the types your code uses, and you can rely on their characteristics not changing (e.g., whether they support inexpensive move operations). When that's the case, you don't need to make assumptions. You can simply look up the move support details for the types you're using. If those types offer cheap move operations, and if you're using objects in contexts where those move operations will be invoked, you can safely rely on move semantics to replace copy operations with their less expensive move counterparts.
Things to Remember
• Assume that move operations are not present, not cheap, and not used.
• In code with known types or support for move semantics, there is no need for assumptions.
Item 30: Familiarize yourself with perfect forwarding failure cases.
One of the features most prominently emblazoned on the C++11 box is perfect forwarding. Perfect forwarding. It's perfect ! Alas, tear the box open, and you'll find that there's “perfect” (the ideal), and then there's "perfect" (the reality). C++11's perfect forwarding is very good, but it achieves true perfection only if you're willing to overlook an epsilon or two. This Item is devoted to familiarizing you with the epsilons.
Before embarking on our epsilon exploration, it's worthwhile to review what's meant by “perfect forwarding.” “Forwarding” just means that one function passes — forwards — its parameters to another function. The goal is for the second function (the one being forwarded to) to receive the same objects that the first function (the one doing the forwarding) received. That rules out by-value parameters, because they're copies of what the original caller passed in. We want the forwarded-to function to be able to work with the originally-passed-in objects. Pointer parameters are also ruled out, because we don't want to force callers to pass pointers. When it comes to general-purpose forwarding, we'll be dealing with parameters that are references.
Perfect forwarding means we don't just forward objects, we also forward their salient characteristics: their types, whether they're lvalues or rvalues, and whether they're const
or volatile
. In conjunction with the observation that we'll be dealing with reference parameters, this implies that we'll be using universal references (see Item 24), because only universal reference parameters encode information about the lvalueness and rvalueness of the arguments that are passed to them.
Let's assume we have some function f, and we'd like to write a function (in truth, a function template) that forwards to it. The core of what we need looks like this:
template
void fwd( T&& param) // accept any argument
{
f( std::forward(param)); // forward it to f
}
Forwarding functions are, by their nature, generic. The fwd
template, for example, accepts any type of argument, and it forwards whatever it gets. A logical extension of this genericity is for forwarding functions to be not just templates, but variadic templates, thus accepting any number of arguments. The variadic form for fwd
looks like this:
template...Ts>
void fwd(Ts&& ...params) // accept any arguments
{
f(std::forward(params) ...); // forward them to f
Читать дальше