Widget&&w2 = widgetFactory();
There are no references to references here, so we're done; w2
is an rvalue reference.
We're now in a position to truly understand the universal references introduced in Item 24. A universal reference isn't a new kind of reference, it's actually an rvalue reference in a context where two conditions are satisfied:
• Type deduction distinguishes lvalues from rvalues.Lvalues of type T
are deduced to have type T&
, while rvalues of type T
yield T
as their deduced type.
• Reference collapsing occurs.
The concept of universal references is useful, because it frees you from having to recognize the existence of reference collapsing contexts, to mentally deduce different types for lvalues and rvalues, and to apply the reference collapsing rule after mentally substituting the deduced types into the contexts in which they occur.
I said there were four such contexts, but we've discussed only two: template instantiation and auto
type generation. The third is the generation and use of typedef
s and alias declarations (see Item 9). If, during creation or evaluation of a typedef
, references to references arise, reference collapsing intervenes to eliminate them. For example, suppose we have a Widget
class template with an embedded typedef
for an rvalue reference type,
template
class Widget {
public:
typedef T&& RvalueRefToT;
…
};
and suppose we instantiate Widget
with an lvalue reference type:
Widget< int&> w;
Substituting int&
for T
in the Widget
template gives us the following typedef
:
typedef int& &&RvalueRefToT;
Reference collapsing reduces it to this,
typedef int&RvalueRefToT;
which makes clear that the name we chose for the typedef
is perhaps not as descriptive as we'd hoped: RvalueRefToT
is a typedef
for an lvalue reference when Widget
is instantiated with an lvalue reference type.
The final context in which reference collapsing takes place is uses of decltype
. If, during analysis of a type involving decltype
, a reference to a reference arises, reference collapsing will kick in to eliminate it. (For information about decltype
, see Item 3.)
Things to Remember
• Reference collapsing occurs in four contexts: template instantiation, auto type generation, creation and use of typedef
s and alias declarations, and decltype
.
• When compilers generate a reference to a reference in a reference collapsing context, the result becomes a single reference. If either of the original references is an lvalue reference, the result is an lvalue reference. Otherwise it's an rvalue reference.
• Universal references are rvalue references in contexts where type deduction distinguishes lvalues from rvalues and where reference collapsing occurs.
Item 29: Assume that move operations are not present, not cheap, and not used.
Move semantics is arguably the premier feature of C++11. “Moving containers is now as cheap as copying pointers!” you're likely to hear, and “Copying temporary objects is now so efficient, coding to avoid it is tantamount to premature optimization!” Such sentiments are easy to understand. Move semantics is truly an important feature. It doesn't just allow compilers to replace expensive copy operations with comparatively cheap moves, it actually requires that they do so (when the proper conditions are fulfilled). Take your C++98 code base, recompile with a C++11-conformant compiler and Standard Library, and — shazam! — your software runs faster.
Move semantics can really pull that off, and that grants the feature an aura worthy of legend. Legends, however, are generally the result of exaggeration. The purpose of this Item is to keep your expectations grounded.
Let's begin with the observation that many types fail to support move semantics. The entire C++98 Standard Library was overhauled for C++11 to add move operations for types where moving could be implemented faster than copying, and the implementation of the library components was revised to take advantage of these operations, but chances are that you're working with a code base that has not been completely revised to take advantage of C++11. For types in your applications (or in the libraries you use) where no modifications for C++11 have been made, the existence of move support in your compilers is likely to do you little good. True, C++11 is willing to generate move operations for classes that lack them, but that happens only for classes declaring no copy operations, move operations, or destructors (see Item 17). Data members or base classes of types that have disabled moving (e.g., by deleting the move operations — see Item 11) will also suppress compiler-generated move operations. For types without explicit support for moving and that don't qualify for compiler-generated move operations, there is no reason to expect C++11 to deliver any kind of performance improvement over C++98.
Even types with explicit move support may not benefit as much as you'd hope. All containers in the standard C++11 library support moving, for example, but it would be a mistake to assume that moving all containers is cheap. For some containers, this is because there's no truly cheap way to move their contents. For others, it's because the truly cheap move operations the containers offer come with caveats the container elements can't satisfy.
Consider std::array
, a new container in C++11. std::array
is essentially a built-in array with an STL interface. This is fundamentally different from the other standard containers, each of which stores its contents on the heap. Objects of such container types hold (as data members), conceptually, only a pointer to the heap memory storing the contents of the container. (The reality is more complex, but for purposes of this analysis, the differences are not important.) The existence of this pointer makes it possible to move the contents of an entire container in constant time: just copy the pointer to the container's contents from the source container to the target, and set the source's pointer to null:
std::vector vw1;
// put data into vw1
…
// move vw1 into vw2. Runs in
// constant time . Only ptrs
// in vw1 and vw2 are modified
auto vw2 = std::move(vw1);
std::array
objects lack such a pointer, because the data for a std::array
's contents are stored directly in the std::array
object:
std::array aw1;
// put data into aw1
…
// move aw1 into aw2. Runs in
Читать дальше