This chapter complements and completes several key topics discussed in the middle parts of the book. In Chapters 11 and 12, for instance, you gained insight into the mechanics behind the copying of objects, that is, copy constructors and assignment operators. And from Chapter 8 you know to prefer pass-by-reference over pass-by-value to avoid undue copying of parameters. And for a long time, that was basically all you needed to know. C++ offered facilities to copy objects, and if you wanted to avoid costly copies, then you simply used either references or pointers. Starting with C++11, however, there is a powerful new alternative. You can no longer only copy objects; you can now also move them.
In this chapter, we’ll show you how move semantics allows you to efficiently transfer resources from one object into another, without deep copying. We also wrap up our treatment of the special class members—of which you already know the default constructor, destructor, copy constructor, and copy assignment constructor.
What the difference is between lvalues and rvalues
That there’s another kind of references: rvalue references
What it means to move an object
How to provide so-called move semantics for objects of your own types
When objects are moved implicitly and how to explicitly move them
How move semantics leads to code that is both elegant and efficient
What impact move semantics has on various best practices regarding defining your own functions and types
The “rule of five” and “rule of zero” guidelines
Lvalues and Rvalues
Every single expression is either an lvalue or an rvalue (sometimes written l-value and r-value and pronounced like that). An lvalue evaluates to some persistent value with an address in memory in which you can store something on an ongoing basis; an rvalue evaluates to a result that is stored only transiently. Historically, lvalues and rvalues are so called because an lvalue expression typically appears on the left of an assignment operator, whereas an rvalue could appear only on the right side. If an expression is not an lvalue, it is an rvalue.1 An expression that consists of a single variable name is always an lvalue.
Note
Despite their names, lvalues and rvalues are classifications of expressions, not of values.
Consider the following statements :
The first statement defines variables a, b, and c as type int and initializes them to 0, 1, and -2, respectively. From then on, the names a, b, and c are all lvalues.
In the second statement, at least in principle, the result of evaluating the expression b + c is briefly stored somewhere, only to copy it into the variable a right after. When execution of the statement is complete, the memory holding the result of b + c is discarded. The expression b + c is therefore an rvalue.
The presence of transient values becomes far more apparent once function calls are involved. In the third statement, for instance, a * c is evaluated first and kept somewhere in memory as a temporary value. This temporary then is passed as an argument to the std::abs() function. This makes that a * c is an rvalue. The int that is returned by std::abs() itself is transient as well. It exists only for an instant, just long enough to be implicitly converted into a double. Similar reasonings apply for the return values of both function calls in the fourth statement—the value returned by std::abs(), for instance, clearly exists only momentarily to serve as an argument to std::pow().
Note
Most function call expressions are rvalues. Only function calls that return a reference are lvalues. One indication for the latter is that function calls that return a reference can appear on the left side of a built-in assignment operator just fine. Prime examples are the subscript operators (operator[]()) and at() functions of your typical container. If v is a vector<int>, for example, both v[1] = -5; and v.at(2) = 132; would make for perfectly valid statements. v[1] and v.at(2) are therefore clearly lvalues.
When in doubt, another good guideline to decide whether a given expression is either an lvalue or an rvalue is the following. If the value that it evaluates to persists long enough for you to take and later use its address, then that value is an lvalue . Here’s an example:
The memory that stores the result of the expressions b + c and std::abs() is reclaimed immediately after the surrounding statements have finished executing. If they were allowed to exist, the pointers x and y would therefore already be dangling before anyone even had the opportunity to look at them. This indicates that these expressions are rvalues. The example also illustrates that all numeric literals are rvalues. The compiler will never allow you to take the address of a numeric literal.
Granted, for expressions of fundamental types, the distinction between lvalue and rvalue rarely matters. This distinction only becomes relevant for expressions of class types and even then only when, for instance, passed to functions that have overloads specifically defined to accept the result of an rvalue expression or when storing objects in containers. The only way for you to truly appreciate these little theory lessons is therefore to bear with us until the next section. There’s just one more concept left to introduce first.
Rvalue References
A reference is a name that you can use as an alias for something else. That much you already know from Chapter 6. What you don’t know yet—how could you?—is that there are actually two kinds of references: lvalue references and rvalue references.
All references that you’ve worked with thus far are lvalue references . Normally, an lvalue reference is an alias for another variable; it is called an lvalue reference because it normally refers to a persistent storage location in which you can store data so it can appear on the left of an assignment operator. We say “normally” because C++ does allow reference-to-const lvalue references—so variables of type const T&—to be bound to temporary rvalues as well. We established as much in Chapter 8 already.
An rvalue reference can be an alias for a variable, just like an lvalue reference, but it differs from an lvalue reference in that it can also reference the outcome of an rvalue expression, even though this value is generally transient. Being bound to an rvalue reference extends the lifetime of such a transient value. Its memory will not be discarded as long as the rvalue reference is in scope. You specify an rvalue reference type using two ampersands following the type name. Here’s an example:
This code will compile and execute, but of course it is definitely not the way to use an rvalue reference, and you should never code like this. This is just to illustrate what an rvalue reference is. The rvalue reference is initialized to be an alias for the result of the rvalue expression count + 3. The output from the next statement will be 8. You cannot do this with an lvalue reference —at least not unless you add a const qualifier . Is this useful? In this case, no, indeed it is not recommended at all; but in a different context, it is very useful. It’s high time you found out when and why, don’t you agree?
Moving Objects
In the running example of this chapter you’ll be working with an Array<> class template similar to that of Ex16_01A. Not unlike std::vector<>, it’s a template for classes that encapsulate and manage dynamically allocated memory. The main difference is that the number of elements that an Array<> object will store always needs to be fixed at construction time, whereas a vector<> has the ability to grow as you add more elements. The definition of this Array<> class template looks like this:
The implementation of all members can remain the same as in Ex16_01A. For now the only thing we add is an extra debug output statement in the copy constructor to track when an Array<> is being copied:
Because the Array<> template of Ex16_01A employs the copy-and-swap idiom, this single output statement also covers the cases where a given Array<> is copied through its copy assignment operator. To refresh your memory, the following is a possible definition of this copy-and-swap assignment operator template. It rewrites copy assignment in terms of the copy construction and a noexcept swap() function:
Using this Array<> class template, we now compose a first example to underline the cost of copying:
Note
This example uses a member function of std::vector<> that you haven’t encountered yet: reserve(size_t). Essentially, it tells the vector<> object to allocate sufficient dynamic memory to accommodate for the given number of elements. These elements will be added, for instance, using push_back().
The program in Ex17_01.cpp constructs a vector<> of 10 Arrays, each containing 1,000 strings. The output of a single run normally looks as follows:
Note
It is possible that your output shows that the Array<> is copied no less than 20 times. The reason is that during each execution of buildStringArray() your compiler might first create the variable result that is defined locally in that function’s body and then copy that Array<> for the first time into the object that is returned by the function. After that, this temporary return value is then copied a second time into an Array<> that is allocated by the vector<>. Most optimizing compilers, however, implement the so-called (named) return value optimization that eliminates the former copy. We’ll discuss this optimization further later in this chapter.
Tip
If Ex17_01 does perform 20 copies for you, the most likely cause is that your compiler is set to use a nonoptimizing, so-called Debug configuration. If so, switching to a fully optimizing Release configuration should resolve the issue. Consult your compiler documentation for more information.
- 1.
The Array<> copy constructor allocates a new block of dynamic memory to hold 1,000 std::string objects.
- 2.
For each of the 1,000 string objects this copy constructor has to copy, it calls the std::string copy assignment operator. Each of these 1,000 assignments in turn allocates one additional block of dynamic memory in which it copies all 73 characters (chars) from the source string.
In other words, your computer has to perform 10 times 1,001 dynamic memory allocations and a fair amount of character copying. In itself this is not a disaster, but it surely feels like all this copying ought to be avoidable. After all, once push_back() returns, the temporary Array<> that buildStringArray() had returned gets deleted and together with it all the strings it used to contain. This means that in total we have copied no less than 10 Arrays and 10,000 string objects and 730,000 characters, only to throw away the originals an instant later!
Imagine being forced to manually copy a 10,000-sentence book—say one of the thicker Harry Potter novels—only to watch someone burn the original immediately afterward. What a total waste of effort! If no one needs the original anymore, why not simply reuse the original instead of creating a copy? To wrap up the Harry Potter analogy, why not give the book a shiny new cover and then pretend that you copied all the pages?
What you need, in other words, is a way to somehow “move” the original strings from inside the transient Array<> into the newly created Array<> object that is held by the vector<>. What you need is a specific “move” operation that does not involve any excessive copying. And if the original Array<> object is to be torn down in the process, so be it. We know it to be a transient object anyway, destined for a Harry Potter bonfire. Before we present you with the modern, now highly recommended solution, we’ll first talk about the programming patterns that were used to avoid such needless memory allocations and copying in pre-C++11 code.
Traditional Workarounds
In older code, one would often use an output parameter to output large objects instead of a return value. We first introduced the concept of output parameters in Chapter 8. For our example, this would mean redefining buildStringArray() as follows:
As argumented in Chapter 8, mixing input and output parameters makes your code harder to understand. What you want is for all function output to be part of the function return value. In modern C++, function parameters should thus mostly be input parameters.
Output arguments do not only lead to code that is less clear, they also make calling a function and using its output rather cumbersome. To illustrate, inside the for loop of Ex17_01.cpp, you’d now need these three lines instead of just one:
Array<std::string> string_array(numStringsPerArray);buildStringArray(string_array);vectorOfArrays.push_back(string_array);You always first need to define the output variable—which also precludes the use of auto by the way!—and cannot embed the function call into the argument list of another function anymore either.
As witnessed by the previous three lines of code, simply reworking the function to use output parameters does not yet eliminated the copy that is being made when entering the Array<> into the vector<>. More changes would be needed. The traditional solution would have you first create all Array<>s in the vector<>—using resize() instead of reserve(), for instance—and then use calls of the form buildStringArray(vectorOfArrays[i]) to fill them with strings. For this to work here, however, we’d first have to rework the Array<> template to no longer require a size at construction time.
But let’s not dwell on this approach any further! The main message should be clear by now. While extra copies and allocations can mostly be avoided using output parameters, the problem is that doing so typically involves a rather awkward coding style. What you really want is code that is clear and intuitive—as close as possible to that of Ex17_01.cpp, ideally—but that at the same time avoids all the expensive copying.
Another traditional workaround is to allocate all Array<> objects in the free store. With this approach, the outline of your code already comes much closer to what you want. Using a C++11 smart pointer, it looks as follows:
However, even if we ignore the extra dynamic allocations, this approach clearly comes with a hefty syntactical overhead. For starters, you’d need a variable of type std::vector<std::unique_ptr<Array<std::string>>>, which is quite the impressive type name already. Also, you’d constantly have to dereference the Array<> pointers through their * and -> operators. Surely there must be a better, more elegant way, right?
Defining Move Members
Thankfully, modern C++ offers precisely what you want. C++11’s move semantics allows you to program in a natural, intuitive style, while at the same time avoiding any unnecessary, costly copy operations. You even do not have to alter the code of Ex17_01.cpp at all. Instead, you extend the Array<> template to ensure that the compiler knows how to instantly move a transient Array<> object into another Array<>, without effectively copying its elements.
For this, we’ll take you back to our earlier example, Ex17_01.cpp. There you studied the following statement:
Given a statement such as this, any C++11 compiler is, of course, well aware that copying the result of buildStringArray() is just silly. After all, the compiler knows full well that it’s a temporary object that is passed to push_back() here and is an object that is scheduled to be deleted right after. So, the compiler knows that you’d prefer not to copy the pushed Array<>. Clearly that’s not the problem. Rather, the problem is that the compiler has no other option but to copy this object. After all, all it has to work with is a copy constructor. The compiler cannot just magically “move” a temporary Array<> object into a new Array<> object—it needs to be told how.
Since the code to construct a copy of an object is defined by the copy constructor of its class (see Chapter 11), it’s only natural that the code to construct a new object from a moved object is defined by a constructor as well. We’ll discuss how you can define one next.
Note
Under the right circumstances, the move members that we’re about to introduce will be generated by the compiler, though clearly not for our Array<> template. For the Array<> template you need to define them yourself explicitly. We’ll explain why that is later in this chapter.
Move Constructors
Here is, one last time, the familiar template for the Array<> copy constructor:
It’s an instance of this constructor template that the push_back() function uses in Ex17_01, which is why each evaluation of this statement involves 1,001 dynamic memory allocations and a whole lot of string copying:
The goal is to write precisely this same line of code but have push_back() use some different constructor instead—one that does not copy the Array<> and its std::string elements. Instead, this new constructor should somehow move all these strings into the new object, without actually copying them. Such a constructor is aptly called a move constructor , and for the Array<> template you can declare one as follows:
The type of the move constructor’s parameter , Array&&, is an rvalue reference. This makes sense; after all, you want this parameter to bind with temporary rvalue results—something regular lvalue references will not do. An lvalue-reference-to-const parameter would, but its const-ness precludes moving anything out of it. When choosing between overloaded functions or constructors, your compiler will always prefer to bind an rvalue argument to an rvalue reference parameter. So whenever an Array<> is constructed with an Array<> rvalue as the only argument (such as our buildStringArray() call), the compiler will call the move constructor rather than the copy constructor.
All that remains now is to actually implement the move constructor. For Array<>, you might do so using this template:
When this constructor is called, moved will be bound to the outcome of an rvalue, in other words, a value that is typically about to be deleted. In any case, the calling code definitely no longer needs the contents of the moved object anymore. And since no one needs it anymore, there’s certainly no harm in you prying the elements array out of the moved object to reuse it for the newly constructed Array<> object. That saves both you and your computer the hassle of copying all the T values.
You thus begin by copying the size and the elements members in the member initializer list. Key here is that you realize that elements is nothing but a pointer of type T*. That is, it’s a variable containing the address of a dynamically allocated array of T elements. And copying such a pointer is not the same as copying the entire array and all its elements. Far from it. Copying a single pointer is of course much, much cheaper than copying an entire array of T values!
Note
This is the difference between what is called a shallow copy and a deep copy . A shallow copy simply copies all members of an object one by one, even if these members are pointers to dynamic memory. A deep copy, on the other hand, copies all dynamic memory referred to by any of its pointer members as well.
Simply performing a member-by-member shallow copy of the moved object is rarely enough for a move constructor. In the case of Array<>, you probably already guessed why. Having two objects point to the same dynamically allocated memory is rarely a good idea, as we explained at length in Chapter 6. The following assignment statement in the body of the Array<> move constructor is therefore of the utmost importance as well:
If you wouldn’t set moved.elements to nullptr, you’d have two different objects pointing to the same elements array. This includes your newly constructed object, clearly, but the transient Array<> moved object still points to that array as well. That would put you right in the middle of the highly volatile minefield that we cautioned you about in Chapter 6, complete with dangling pointers and multiple deallocations! By setting the elements pointer of the moved object to null, the destructor of the temporary object bound to moved will effectively perform delete[] nullptr, which as you know is harmless. Phew! Mines defused.
You should plug this move constructor into the Array<> template of Ex17_01 and then run the program again (the resulting program is available in Ex17_02). The output then normally becomes as follows:
Instead of 10,000 string objects and three-quarters of a million char values, only ten pointers and ten size_t values have now been copied. As performance improvements go, that’s not too shabby! Certainly it’s well worth the effort of defining a few extra lines of code, wouldn’t you agree?
Move Assignment Operators
Just like a copy constructor is normally accompanied by a copy assignment operator (see Chapter 12), a user-defined move constructor is typically paired with a user-defined move assignment operator . Defining one for Array<> should be easy enough for you at this point:
The only new thing here is the rvalue reference parameter rhs, designated using the double ampersands: &&. The operator’s body itself simply contains a mix of elements that you’ve already seen before, either in the copy assignment operators of Chapter 12 or in the move constructor of Array<>.
If present, the compiler will use this assignment operator rather than the copy assignment operator whenever the object on the right side of the assignment is a temporary Array<> object. One case where this would occur is in the following snippet:
The Array<std::string> object returned by buildStringArray() is again clearly a temporary object, so the compiler realizes that you’d prefer not to copy it and will therefore pick the move assignment over the copy assignment operator. You can see this assignment operator in action if you add it to the Array<> template of Ex17_02 and use that together with this program:
You can use the same definition of the buildStringArray() function as in Ex17_01 and Ex17_02. The output of this program should reflect that assigning an rvalue to the strings variable indeed results in a call to the move assignment operator , whereas assigning the lvalue more_strings to the same variable results in a call to the copy assignment:
Explicitly Moved Objects
The main() function of Ex17_03.cpp ended with the following two statements:
Running Ex17_03 revealed that this final assignment resulted in the more_strings Array<> being copied. The reason, of course, is that more_strings is an lvalue. A variable name always is (remember this, it’s important!). In this case, however, the fact that more_strings gets copied is actually quite unfortunate. The culprit assignment is the very last statement of the function, so there’s clearly no need for more_strings to persist. And even if the main() function would have continued beyond this point, it would still be perfectly plausible that this assignment would be the last statement to ever reference more_strings . In fact, it is fairly common for a named variable such as more_strings to no longer be needed once its contents have been handed over to another object or to some function. It would be a real shame if “handing over” a variable such as more_strings could be done only by copying just because you gave the variable a name?
C++11 foresees a solution for this: you can turn any lvalue into an rvalue reference simply by applying one of C++11’s most vital Standard Library functions: std::move().2 To see its effect, you should first replace the final two lines of main() in Ex17_03.cpp with the following and then run the program again (you can find this variant in Ex17_04):
If you do, you will notice that more_strings is indeed no longer copied:
Move-Only Types
No discussion of std::move() is complete without an honorary mention of std::unique_ptr<>—undoubtedly the type whose variables you’ll be moving the most in modern C++ programming. As explained in Chapter 6, and again in Chapter 12, there must never be two unique_ptr<> smart pointers pointing to the same address in memory. Otherwise, both would delete (or delete[]) the same raw pointer twice, which is a perfect way to initiate a tragic failure of your program. It is thus only fortunate that neither of the two lines that are commented out at the end of the following code snippet compiles:
What would compile, however, are these two lines:
That is, while both copy members of std::unique_ptr<> are deleted (as explained in Chapter 12), both its move assignment operator and its move constructor are present. This is an outline of how you’d normally accomplish this:
To define a move-only type, you always start by deleting its two copy members (like we taught you in Chapter 12). By doing so, you implicitly delete the move members as well (more on this later), so to allow uncopiable objects to be moved, you must explicitly define the move members. Often, but not in the case of unique_ptr<>, it would suffice to use = default to define the two move members and have the compiler generate them for you.
Extended Use of Moved Objects
An lvalue expression by definition evaluates to a persistent value, which is a value that remains addressable after the execution of the lvalue’s parent statement. This is why the compiler normally prefers to copy the value of an lvalue expression. We just told you, however, how you can overrule this preference with std::move() . That is, you can force the compiler to pass any object to a move constructor or move assignment operator, even if it’s not a temporary. This raises the question, what happens if you keep using an object after it has been moved? Here’s an example (if you want, you can try this in Ex17_04):
In this specific case, you of course already know what will happen. After all, you’ve written the code for the move assignment operator of Array<> yourself. Once moved, an Array<> object will contain an elements pointer that is set to nullptr. Any further use of a moved Array<> then would trigger a null pointer dereference, which, as always, is destined to end in the tragic, gruesome death of your program. This example therefore nicely underlines the rationale behind this guideline:
Caution
As a rule, you should only move an object if you are absolutely sure it is no longer required. Unless otherwise specified, you’re not supposed to keep on using an object that was moved. By default, any extended use of a moved object results in undefined behavior (read: it’ll result in fatal crashes).
This guideline applies for objects of Standard Library types as well. Completely analogous to Arrays, for instance, you must never simply keep using a moved std::vector<>. Doing so could very well end equally badly. You might hope that a moved vector<> is equivalent to an empty one. And while this may be so for some implementations, in general the C++ standard does not specify at all in which state a moved vector<> should be. What is (implicitly) allowed, however, and which is something that not too many developers will know, is this:
Tip
If need be, you can safely revive a move()’d vector<> by calling its clear() member. After calling clear(), the vector<> is guaranteed to be equivalent to an empty vector<> and thus safe to use again.
In the rare cases where you do want to reuse moved Standard Library objects, you should thus always check the specification of their move members. When a std::optional<T> object that contains a T value is moved, for instance, it will still contain a T value—a T value that is now moved (we covered std::optional<> in Chapter 8). Similar to a vector<>, the optional<> can safely be reused after calling its reset() member. Smart pointers are a scarce exception:
Tip
The Standard Library specification clearly stipulates that you may continue using smart pointers of type std::unique_ptr<> and std::shared_ptr<> after moving out the raw pointer, and this without first calling reset(). For both types, move operations must always set the encapsulated raw pointer to nullptr.
A Barrel of Contradictions
Many find move semantics confusing at first, and understandably so. We hope this section can somewhat spare you from this fate, as it aims to alleviate some of the most common sources of confusion.
std::move() Does Not Move
Make no mistake, std::move()does not move anything. All this function does is turn a given lvalue into an rvalue reference. std::move() is effectively nothing more than a type conversion, not at all unlike built-in cast operators static_cast<>() and dynamic_cast<>(). In fact, you could almost implement your own version of the function simply like so (somewhat simplified):
Clearly, this moves nothing. What it does do, however, is make a persistent lvalue eligible for binding with the rvalue reference parameter of a move constructor or assignment operator. It’s these member functions then that are supposed to move the members of the former lvalue into another. std::move() only performs a type cast to make the compiler pick the right function overload.
If no function or constructor overload exists with an rvalue reference parameter, an rvalue will happily bind with an lvalue reference instead. That is, you can move() all you want; but if there’s no function overload standing ready to receive the resulting rvalue, it’s all in vain. To demonstrate this, we’ll again start from Ex17_04.cpp. As you may recall, this latest variant of our running example ended with these statements:
Obviously, the intent of this final statement is to move the 2,000 strings of more_strings into strings. And in Ex17_04 this worked flawlessly. But now see what happens if you remove the declaration and definition of the move assignment operator from its Array<> template. If you then run the main() function of Ex17_04.cpp once more, unaltered, the last line of the output reverts to this:
It does not matter that you still call std::move() in the body of main(). If there’s no move assignment operator for Array<> to accept the rvalue, the copy assignment operator will be used instead. So, always remember, adding std::move() is of no consequence if the function or constructor that you are passing a value to has no overload with an rvalue reference parameter!
An Rvalue Reference Is an Lvalue
To be precise, the name of a named variable with an rvalue reference type is an lvalue. Let’s show what we mean by this, as it turns out that many struggle with this idiosyncrasy at first. We’ll keep on milking the same example, naturally, so take Ex17_04 once again (make sure it contains the original Array<> template where the move assignment operator is still defined) and change the last two lines of the program to the following:
Notwithstanding that the rvalue_ref variable clearly has an rvalue reference type, the output of the program will show that the corresponding object is copied:
Every variable name expression is an lvalue, even if the type of that variable is an rvalue reference type. To move the contents of a named variable, you must therefore always add std::move():
While you’d normally not create an rvalue reference in the middle of a block of code like rvalue_ref in our latest example, the analogous situation does occur regularly if you define function parameters that are rvalue references . You can find an example of this in the next section.
Defining Functions Revisited
In this section we’ll study how move semantics has influenced best-practice guidelines for defining new functions, complementing what you saw earlier in Chapter 8. We’ll answer questions such as these: How does move semantics affect the choice between pass-by-reference and pass-by-value? Or, to return an object by value without copying, should I use std::move() or not? You’ll find both answers coming up. We kick off with investigating how and when to pass arguments by rvalue reference to regular functions, that is, to functions that are not move constructors or move assignment operators.
Pass-by-Rvalue-Reference
In Exercise 16-1, you were tasked with defining a push_back() member function template for Array<>. If all went well, you created a function similar to this:
- 1.
To avoid redundant copies of the arguments passed to push_back(), you defined the template using a reference-to-const parameter—precisely like we taught you in Chapter 8.
- 2.
In the function body, you used the copy-and-swap idiom to make sure that the push_back() member behaves as desired even if the function body were to throw an exception (std::bad_alloc, for instance, or any other exception that T’s copy assignment might throw). We refer you to Chapter 16 for a detailed discussion of copy-and-swap.
The part we’d like to draw your attention to here is the blatant amount of copying that this function performs. First, all existing elements are copied into the new, larger Array<> and then the newly added element as well. While still excusable for a Chapter 16 exam, in your Chapter 17 exam such needless copying will start costing you grades . And in this case it is not because copying on your exam is cheating, but because clearly you should at least try to move all these elements instead!
Fixing the loop that copies all existing elements appears easy enough. Simply apply std::move() to turn the lvalue elements[i] into an rvalue, right? Doing so clearly avoids all copies, provided that the template argument type T has an appropriate move assignment operator:
Caution
If the sidebar sections in Chapter 16 taught you anything, however, it’s that appearances can be deceiving. Yes, adding std::move() like this works like a charm in the nominal case. But that’s not the end of it. In general, this implementation is ever so slightly flawed. We’ll reveal what this minor imperfection is later in this chapter. For now this initial version will do its job just fine. Adieu, gratuitous copying!
Now that all existing elements have been move()’d, let’s focus our attention on the newly added element. Omitting all irrelevant parts, this is thus the code we will consider next:
Your first instinct might be to simply slap another std::move() onto element, like so:
This will not work, though, and with good reason. element is a reference to a const T, meaning that the caller of the function expects the argument not to be modified. Moving its contents into another object is therefore completely out of the question. This is also why the std::move() type conversion function will never cast a const T or const T& type to T&&. Instead, std::move() converts it to the rather pointless type const T&&—a reference to a transient value that you’re not allowed to modify. In other words, because the type of element is const T&, the type of std::move(element) is const T&&, meaning that assigning the latter expression still goes through the copy assignment operator, despite the std::move().
But of course you still want to cater for those cases where the caller does not need the element argument anymore—that is, for those cases where push_back() is called with an rvalue. Luckily, you cannot only use rvalue reference parameters for move constructors and move assignment operators; you can use them for any function you want. And so you could easily add an extra overload of push_back() that accepts rvalue arguments:
Caution
When passing an argument such as element by rvalue reference, never forget that inside the function’s body the expression element is again an lvalue. Remember, any named variable is an lvalue, even if the variable itself has an rvalue reference type! In our push_back() example, this means that the std::move() in the function’s body is very much required to compel the compiler into choosing T’s move assignment operator rather than its copy assignment operator.
The Return of Pass-by-Value
The introduction of move semantics has caused a significant shift in how you can and should define the parameters of certain functions. Before C++11, life was easy. To avoid copies, you simply always passed objects by reference, just like we taught you in Chapter 8. Now that support for move semantics is prevalent, however, pass-by-lvalue-reference is no longer always your best option. In fact, it turns out that passing arguments by value is back on the table, at least in some specific cases. To explain when and why, we’ll build further on the same push_back() example as just now.
In the previous subsection, we created two separate overloads of push_back(), one for const T& and one for T&& references . For your convenience, you can find this variant in Ex17_05A. While using two overloads is certainly a viable option, it does imply some tedious code duplication. One way to work around this duplication is to redefine the const T& overload in terms of the T&& one like so:
But there is an even better, more compact way, one where one single function definition is all you need. Somewhat surprisingly, this single push_back() definition will use pass-by-value. You’d probably never have guessed to replace two pass-by-reference overloads with one single definition that uses pass-by-value, but once you see it in action, you’ll surely appreciate the sheer elegance of this approach. If we again omit the irrelevant, this is what your new push_back() would look like:
Provided you only use types that support move semantics (such as Standard Library types), this function always does precisely what you want. It does not matter what kind of argument you pass it: lvalue arguments are copied, once, and rvalue arguments are moved. Because the function parameter is of value type T, a new object of type T is created whenever push_back() is called. The beauty is that in order to construct this new T object, the compiler will use a different constructor depending on the kind of argument. If the function’s argument is an lvalue, the element argument is constructed using T’s copy constructor. If it’s an rvalue, however, element will be constructed using T’s move constructor.
Caution
For a generic container such as Array<>, you probably still want to take into account the possibility that the type T does not provide a move constructor or assignment operator. For such types, the variant that accepts T by value would actually copy any given argument twice. Remember, std::move() does not move unless T has a move assignment operator. If it doesn’t, the compiler will quietly revert to the copy assignment operator to evaluate newArray[size] = std::move(element). In real life, a container like Array<> would thus mostly still have both overloads of push_back()—one for lvalues and one for rvalues.
The only reason that using pass-by-value is interesting here is because push_back() always and inevitably copies a given lvalue. Since a copy is thus inevitable anyway, you might as well create this copy already when constructing the input argument. Arguments that do not need to be copied by a function should of course still be passed by reference-to-const!
For nontemplated functions that inherently copy any lvalue input, however, the use of a single function with a pass-by-value parameter is a wonderfully compact alternative. Concrete examples would be functions with signatures such as setNumbers(std::vector<double> values) or add(BigObject bigboy). The traditional reflex would be to use pass-by-reference-to-const, but by passing the arguments by value instead, you can actually kill two birds with one stone. If defined like this, the same single function can handle both lvalues and rvalues with near-optimal performance!
To demonstrate that this effectively works, simply add the push_back(T) function we laid out here into the Array<> template of Ex17_04. Now that Array<> supports push_back(), it also makes sense to give the arraySize parameter of its constructor a default value of zero:
All other members can remain as they were in Ex17_04. With this, you can compose the following example program (the definition of buildStringArray() can be taken from Ex17_04 as well):
In main(), we create an Array of Arrays of strings, aptly named array_of_arrays. We begin by inserting an Array<> of 1,000 strings into this container. This Array<> element, aptly named array, is clearly pushed as an lvalue, so we expect it to be copied. That is a good thing: we still need its contents for the remainder of the program. At the end of the program, we add array a second time, but this time we first convert it into an rvalue by applying std::move() to the variable’s name. We do so in the hopes that the Array<> and its string array will now be moved instead of copied. Running Ex17_05B confirms that this is indeed the case; in total, array is copied only once:
The various move assignments occur in the body of the push_back() function. In between adding array as an lvalue and adding it again as an rvalue, we inject one extra string into it to be able to differentiate the two Array<> elements in the output. Note that in the process of pushing this additional string none of the 1,000 preexisting string elements inside array are copied either. They are all moved to a larger string Array<> through the move assignment operator of std::string!
In the following tip, we summarize the various guidelines regarding function parameter declarations that you have encountered throughout the book, mainly in Chapter 8 and here:
Tip
For fundamental types and pointers, you can simply use pass-by-value. For objects that are more expensive to copy, you should normally use a const T& parameter. This avoids any lvalue arguments from being copied, and rvalue arguments will bind just fine with a const T& parameter as well. If your function inherently copies its T argument, however, you should pass it by value instead, even when it concerns a large object. Lvalue arguments will then be copied when passed to the function, and rvalue arguments will be moved. The latter guideline presumes that the parameter types support move semantics—as all types should these days. In the less likely case that the parameter type lacks proper move members, you should stick with pass-by-reference. More and more types support move semantics these days , though—not in the least all Standard Library types—so pass-by-value is most certainly back on the table!
Return-by-Value
The recommended way for returning an object from a function has always been to return it by value; even for larger objects such as a vector<>. As we’ll discuss shortly, compilers have always been good at optimizing away any redundant copies of objects that you return from a function. The introduction of move semantics therefore doesn’t change anything for the best practices. There are many misconceptions in this area (believe us, there are!), however, so it remains well worth reviewing this aspect of defining functions as well.
Many sample programs in this chapter were built around the following function:
In a return statement of the form return name;, a compiler is obliged to treat name as if it were an rvalue expression, provided name is either the name of a locally defined automatic variable or that of a function parameter.
In a return statement of the form return name;, a compiler is allowed to apply the so-called named return value optimization (NRVO), provided name is the name of a locally defined automatic variable (not if it is that of a parameter name).
For our example, NRVO would entail that the compiler stores the result object directly in the memory designated to hold the function’s return value. That is, after applying NRVO, no memory is set aside anymore for a separate automatic variable named result.
If the type of the object that you return has no move constructor, then adding std::move() causes the compiler to fall back to the copy constructor! Yes, that’s right. Adding move() can cause a copy, where before the compiler would probably have applied NRVO.
Even if the returned object can be moved, adding std::move() can only make matters worse—never better. The reason is that NRVO generally leads to code that is even more efficient than move construction (move construction typically still involves some shallow copying and/or other statements; NRVO does not).
So, adding std::move() at best makes things a little bit slower, and at worst it causes the compiler to copy the return value where it otherwise would not! Therefore:
Tip
If value is either a local variable (with automatic storage duration) or a function parameter, you should never write return std::move(value);. Always write return value; instead.
Note that for return statements such as return value + 1; or return buildStringArray(100); you never have to worry about adding std::move() either. In both these cases you are already returning an rvalue, so adding std::move() would again be redundant.
If the variable value in return value; has static or thread-local storage duration (see Chapter 10), you need to add std::move() if moving is what you want. This case is rare, though.
When returning an object’s member variable, as in return member_variable;, std::move() is again required if you do not want the member variable to be copied.
If the return statement contains any other lvalue expression besides the name of a single variable, then NRVO does not apply, nor will the compiler treat this lvalue as if it were an rvalue when looking for a constructor.
Common examples for the latter case are return statements of the form return condition? var1 : var2;. While not obvious, a conditional expression such as condition? var1 : var2 is in fact an lvalue. Because it’s clearly no variable name, the compiler forsakes NRVO and will not implicitly treat it as an rvalue either. It will, in other words, look for a copy constructor to create the returned object (either var1 or var2). To avoid this, you have at least three options. Any of the following return statements will at least attempt to move the value that is returned:
Out of these three, the last one is most recommended. The reason is again that it allows a clever compiler to apply NRVO, something that with the former two forms is not allowed.
Defining Move Members Revisited
Now that you are a move semantics expert, we can give some more advice on how to properly define your own move constructors and move assignment operators. Our first guideline—that is, to always declare them as noexcept—is particularly important. Without noexcept, your move members are not nearly as effective. (The noexcept specifier was explained in Chapter 16.)
Always Add noexcept
It is important that all your move members have a noexcept specifier, assuming they do not throw, of course, though in practice move members rarely do. Adding noexcept is so important that we’ll even go out of our way to explain why this is so. The reason is that we firmly believe that knowing why a guideline exists makes you remember it all the better!
For this, let’s pick up where we left off earlier with Ex17_05B. In this example, you defined the following push_back() function for Array<>:
Looks good, right? Running Ex17_05B also confirmed that all redundant copies are gone. So, what’s wrong with this definition then? In Chapter 16 we cautioned you to look beyond apparent correctness. For one, we urged you to always consider what happens in case of unexpected exceptions. Any idea as to what could be wrong with push_back()? It might be good for you to reflect on this for a moment.
The answer is that, while unlikely, the move assignment operator of T could in principle throw an exception. This is especially relevant here since the Array<> template should work for any type T. Now consider what happens if such an exception occurred in the middle of the function’s for loop, or even while moving the new element?
In a way, adding the std::move() in the for loop has undermined the workings of the copy-and-swap idiom. With this idiom it is crucial that the object you are modifying (*this, typically, in the case of a member function) remains in pristine, untouched condition, right up to the final swap() operation. Any modification prior to the swap() does not get undone in case of an exception. So if an exception occurs inside push_back() while moving one of the objects in the elements array, there’s nothing in place to restore any earlier objects that were already moved into newArray.
It turns out that, no matter what you try, if a move member may throw at any time, there is in general no way for you to safely move the existing elements into the new array without copying. If only there was a way for you to know that a given move member never throws any exceptions. After all, if moving never throws, safely moving existing elements into a larger array becomes feasible. But, hang on. Of course, you know of such a way already! It was introduced in Chapter 16: the noexcept specifier. If a function is noexcept, you know for a fact that it will never throw an exception. In other words, what you want to somehow express in push_back() is this:
The move_assign_if_noexcept() function we need here to accomplish an efficient yet safe push_back() template should act as std::move(), yet only if the move assignment operator of T is specified to be noexcept. If not, move_assign_if_noexcept() should turn its argument in an lvalue reference instead, thus triggering the use of T’s copy assignment.
This is where we hit a minor bump in the road:
Caution
The Standard Library utility header does provide std::move_if_noexcept(), but reading the fine print reveals that this function is intended to conditionally invoke either a move constructor or a copy constructor depending on whether the move constructor is noexcept. The Standard Library offers no equivalent for conditionally invoking a move assignment operator.
While implementing such a move_assign_if_noexcept() function is certainly possible, it unfortunately requires a technique that is actually way too advanced for a beginner’s book; concretely, it requires template metaprogramming. Before we go there, here’s an important conclusion already:
Caution
If nothing else, what the Array<>::push_back() function should teach you is to never give in to the temptation to implement your own container classes, unless this is absolutely necessary (and it rarely is!). Getting them 100 percent right is deceptively hard. Always use containers of the Standard Library instead (or those of other tested and tried libraries if you prefer). Even with the move_assign_if_noexcept() function in place, your Array<> class can hardly be called optimal. You’d need to rework it considerably using rather advanced memory management techniques to even get remotely close to a fully optimized std::vector<>!
The upcoming sidebar briefly explains how you could implement the move_assign_if_noexcept() function required for our exception-safe push_back() member (you can find the result in Ex17_06). It involves template metaprogramming, so it is not for the faint-hearted. So, please feel free to skip straight ahead to the next regular subsection, where we’ll demonstrate the effect of adding noexcept to move members when using your types in Standard Library containers.
Implementing Move_Assign_If_Noexcept
Template metaprogramming typically involves making decisions based on template arguments (which, as you know, are often type names) to control the code that is generated by a template when it is instantiated by the compiler. In other words, it involves writing code that is evaluated at compile time, whenever the compiler generates a concrete instance of the corresponding template. In the case of move_assign_if_noexcept() , what we essentially need to encode is the equivalent of a regular C++ if-else statement that expresses the following logic for the function’s return type:
“If rvalues of type T&& can be assigned without throwing, then the return type should be an rvalue reference (that is, T&&); otherwise, it should be an lvalue reference instead (const T&).”
T here is the template type argument to move_assign_if_noexcept() . Without further ado, this is how you’d convey precisely this logic using some of the template metaprogramming primitives—so-called type traits—provided by the type_traits Standard Library header:
To a template metaprogrammer, this reads almost word for word as what we said earlier. It takes some (read: a lot of) getting used to, though, which obviously is something we don’t have time for now. We therefore won’t dwell on this any further either. The aim of this sidebar, after all, is just to give you a quick first taste of what is possible with template metaprogramming.
With the previous meta-expression, composing a fully functioning move_assign_if_noexcept() function is actually not that hard anymore:
The function body is trivial; all template meta-magic happens in the return type of the function. Depending on the properties of type T—more concretely, depending on whether T&& values can be assigned without throwing—the return type will be either T&& or const T&. Note that in the latter case, it does not matter that the std::move() in the function’s body still returns an rvalue reference. If the return type in the template’s instantiation for type T is const T&, then the rvalue reference that is returned from the function body gets turned right back into an lvalue reference .
Moving Within Standard Library Containers
Naturally all container types of the Standard Library are optimized to move objects whenever possible, just like you did for the Array<> container template. This means that any implementation of std::vector<> faces challenges analogous to those you faced with push_back(). Namely, how does one guarantee internal integrity in the presence of exceptions when moving existing elements into a newly allocated, larger array? How then does one guarantee the desired all-or-nothing behavior—a behavior the C++ standard normally requires for all container operations?
We can deduce how the Standard Library implementers approached these challenges using the following variant of Ex17_05B:
Instead of adding the Array<>s to an Array<>, we now add them to a std::vector<>. More concretely, we add two rvalues of type Array<>&& to a single vector<> (the implementation of buildStringArray() is the same as always). One possible sequence of events then is this (provided you started with an Array<> template where the move members do not yet have the necessary noexcept specifiers, that is):
From this output, you clearly see that when adding the second element (the Array<> with 2,000 elements), the std::vector<> copies the first element (the Array<> with 1,000 strings). Its push_back() member does so while transferring all existing elements to a larger dynamic array, similar to what we did earlier in Array::push_back().
Note
The Standard Library specification does not explicitly prescribe when and how often a vector<> should allocate a larger dynamic array as you add more elements. So, it could in principle be that with Ex17_07.cpp you do not yet see the Array<> of 1,000 elements being copied. If so, just add more push_back() statements to add extra Array<>&& elements. Because the Standard Library does require a vector<> to store all its elements in one contiguous dynamic array, eventually the vector<> inevitably has to allocate a larger array and then transfer all its existing elements into that.
The reason that the Array<> is copied rather than moved is that the compiler deemed moving to be unsafe. That is, it could not deduce that moving is possible without throwing. Naturally, if you now add noexcept to your Array<> move constructor template and run Ex17_07 again, you’ll find that the Array<> and its 1,000 strings are no longer copied:
Tip
Standard containers and functions typically only exploit move semantics if the corresponding move members are declared with noexcept. Whenever possible—and it almost always is—it is therefore crucial that all your move constructors and move assignment operators are declared noexcept.
The “Move-and-Swap” Idiom
Before we move on to the final section of this chapter, we’ll first revisit the implementation of the Array<> move assignment operator. When we defined it earlier, near the beginning of the chapter, you didn’t know enough yet to implement this operator in a clean and elegant manner. Going back to this definition, isn’t there something that strikes you as suboptimal? We’ll throw in the noexcept specifier to get things going, as shown here:
What if we told you that you’re looking for code duplication?
First, it contains the same logic as the destructor to clean up any existing members. In our case this is just a single delete[] statement, but in general this could require any number of steps.
Second, it contains the same logic as the move constructor to copy all members and to set the elements member of the moved object to nullptr.
In Chapter 16 we told you that it’s always good practice to avoid duplication as much as reasonably possible. Any duplication, even that of only one or a couple of lines of code, is just another opportunity for bugs—either now or in the future. Just imagine that one day you add an extra member to Array<>; it would then be easy to forget to update all places where they’re copied one by one. The fewer places you need to update, the better.
For copy assignment operators, you are already familiar with the standard technique to solve the analogous problem: the copy-and-swap idiom. Fortunately, you can use a similar pattern for move assignment operators as well:
This move-and-swap idiom eliminates both accounts of duplication. Any existing elements are deleted by the destructor of the automatic variable moved, and the responsibility of copying the members and assigning nullptr is delegated to the move constructor. Key is not to forget the explicit std::move() of rhs in the function body. Remember, and we cannot repeat this enough, a variable name is always an lvalue, even the name of a variable with an rvalue reference type!
Take care not to go too far in weeding out duplication. Given earlier guidelines, you may be tempted to reduce code duplication even more and combine the copy and move assignment operators into one single assignment operator of the following form:
At first sight, this looks like a nice and elegant improvement. Whenever the right side of an assignment is a temporary object, the rhs value argument to this assignment operator will be constructed and initialized using the move constructor. Otherwise, the copy constructor is used. The problem, however, is that it violates another guideline, which is that the move assignment operator should always be noexcept. Otherwise, you risk that the copy assignment is still used by containers and other Standard Library templates that move only when noexcept is present.
The following guideline thus summarizes our advice regarding defining assignment operators:
Tip
If you define them at all (see later), always define separate copy and move assignment operators. The latter should be noexcept, the former typically not (copying typically risks triggering a bad_alloc exception, at the least). To avoid additional duplication, the copy assignment operator should use the traditional copy-and-swap idiom, and the move assignment operator should use the analogous move-and-swap idiom we introduced here.
Finally, the copy assignment operator should always use pass-by-reference-to-const, not pass-by-value. Otherwise, you risk running into compiler errors because of so-called ambiguous assignments.
Special Member Functions
The quality that makes them “special” is that, under the right circumstances, the compiler is kind enough to generate them for you. It’s interesting to note what the compiler may provide you with a simple class. Here’s a class with just a single data member:
What you actually get is the following, assuming your compiler conforms to the current language standard:
Default-generated move members are something we haven’t discussed yet, so we’ll start there. Once they are covered, the remainder of this section then reviews when exactly you should define your own versions of these special functions.
Default Move Members
Analogous to the two default copy members (see Chapters 11 and 12), compiler-generated move members simply move all non-static member variables one by one, in the order in which the member variables are declared in the class definition. If the class has base classes, their move constructors or move assignment operators are called first, again in the order in which the base classes are declared. An implicitly defined move member, finally, is always noexcept, as long as the corresponding member function is noexcept for all base classes and non-static member variables as well.
So, no surprises there. If the move members are defined by the compiler, they behave exactly as you’d expect. The main questions we still need to answer are as follows: When exactly does the compiler generate these default move members? And, why did it for instance not do so for our Array<> class template? The answer is as follows:
Tip
As soon as you declare either any of the four copy or move members or a destructor, the compiler will no longer generate any missing move members.
While this rule may appear fairly restrictive at first, it actually makes a lot of sense. Consider our original Array<> template. The compiler observes that you explicitly defined the destructor, copy constructor, and copy assignment operator. The only sensible reason for you to provide such explicit definitions is, of course, because the compiler-generated defaults would be wrong. From this, the compiler can draw only one sensible conclusion: if it were to generate default move members, they would almost certainly be wrong as well (note that for Array<>, this reasoning is most definitely sound!). When in doubt, generating no default move members at all is clearly always better than generating incorrect ones. After all, the worst that could happen to an object without move members is that it gets copied from time to time; this is a fate that pales in comparison to what might happen to objects with incorrect move members!
The Rule of Five
Naturally, whenever you define a move constructor, you should also define its companion, the move assignment operator—and vice versa. From Chapter 12 you’ll recall that the same holds when defining the copy members. These observations are generalized in the so-called rule of five. This best-practice guideline concerns the following five special member functions: the copy constructor, copy assignment operator, move constructor, move assignment constructor, and destructor. In other words, it applies to all special member functions except for the default constructor. The rule goes as follows:
Rule of Five
As soon as you declare any of the five special member functions other than the default constructor, you should normally declare all five of them.
The motivation, not by chance, is analogous to that in the previous subsection. As soon as you need to override the default behavior for any one of these five special functions, you almost certainly need to do so for the other four as well. For instance, if you need to delete or delete[] a member variable memory in the destructor, it stands to reason that a shallow copy of the corresponding member would be extremely dangerous. The converse is perhaps less obvious but generally holds as well.
Notice that the rule of five does not state that it is required to actually provide explicit definitions for all five special member declarations. It is perfectly OK to use = delete at times (for instance when creating uncopiable types—as shown in Chapter 12) or even = default (typically combined with some = delete definitions as well).
The Rule of Zero
The rule of five governs what to do if and when you declare your own special member functions. It does not say anything, however, about when you should do so. This is where the rule of zero comes in:
Rule of Zero
Avoid having to implement any of the special member functions as much as possible.
In a way, the rule of zero is what mathematicians would call a corollary of the rule of five (a corollary is a logical consequence of an already proven proposition). After all, the latter rule stipulates that defining any special member function instantly means defining five of them, as well as perhaps a swap() function, as you already know. Even with the copy-and-swap and move-and-swap idioms, defining these five members always involves writing a significant amount of code. And a significant amount of code means a significant number of opportunities for bugs and a significant maintenance overhead.
The usual motivational example is to consider what you’d need to do after adding a new member variable to an existing class. In how many places then do you need to add a line of code? One more line in the member initializer list of the copy constructor? And one in the move constructor as well? And, oh yes, we almost forgot about the extra line in swap()! Whatever the count is, nothing beats zero! That is, in an ideal world, all you’d have to do to add a new member variable is to add the corresponding declaration to the class definition. Nothing more, nothing less.
All dynamically allocated objects should be managed by a smart pointer (Chapter 4).
All dynamic arrays should be managed by a std::vector<> (Chapter 5).
More generally, collections of objects are to be managed by container objects such as those provided by the Standard Library (see also Chapter 19).
Any other resources that need cleanup (network connections, file handles, and so on) are managed by a dedicated RAII object as well (Chapter 16).
If you simply apply these principles to all member variables of your class, none of these variables should contain so-called raw or naked resources anymore. That normally implies that none of the five special member functions that the rule of five speaks about requires an explicit definition anymore either.
One consequence of respecting the rule of zero is that defining the rule of five member functions becomes reserved almost exclusively for when you define either a custom RAII type or (and this should be even rarer) a custom container type. Afterward, with these custom RAII and container types in place, you can then compose higher-level types that then mostly need no explicit copy, move, or destructor declarations of their own anymore.
There is one special member function we haven’t covered yet with the previous guidelines. This is the same one that is not covered by the rule of five: the default constructor. You can typically avoid having to define a default constructor by initializing all member variables in your class definition. We did this, for instance, for the Data class at the beginning of this section:
Mind you, you’ll recall from Chapter 11 that as soon as you declare any constructor, even one that is not a special member function, the compiler will no longer generate a default constructor. The rule of zero therefore most definitely does not preclude defaulting a default constructor like this:
Summary
This chapter, in more ways than one , wrapped up that which we started in Chapters 10 and 11. You learned what move semantics is and how it allows for natural, elegant, and—most importantly, perhaps—efficient code. We taught you how to facilitate moving for your own types. The idea of move operations is that since the argument is temporary, the function doesn’t necessarily need to copy data members; it can instead steal the data from the object that is the argument. If members of the argument object are pointers, for example, the pointers can be transferred without copying what they point to because the argument object will be destroyed and so doesn’t need them.
An rvalue is an expression that typically results in a temporary value; an lvalue is one that results in a more persistent value.
std::move() can be used to convert an lvalue (such as a named variable) into an rvalue. Take care, though. Once moved, an object should normally not be used anymore.
An rvalue reference type is declared using a double ampersand, &&.
The move constructor and move assignment operator have rvalue reference parameters, so these will be called when the argument is a temporary (or any other rvalue).
If a function inherently copies one of its inputs, passing this argument by value is preferred, even if this concerns an object of a class type. By doing so, you can cater for both lvalue and rvalue inputs with one single function definition.
Automatic variables and function parameters should be returned by value and without adding std::move() to the return statement.
Move members should normally be noexcept; if not, they risk not being invoked by Standard Library containers and other templates.
The rule of five entails that you either declare all copy members, move members, and the destructor together, or none of them at all. The rule of zero urges you to strive to define none at all. The means to achieve rule of zero compliance you actually already know: always manage dynamic memory and other resources using smart pointers, containers, and other RAII techniques!
Exercises
Exercise 17-1. Define move operators for the Truckload class (the last time you encountered this class was in Exercise 15-3) and provide a small test program to show that it works.
Exercise 17-2. Another class that desperately needs to be upgraded with moving capabilities is the LinkedList<> template you defined for Exercise 16-5. It could even do with more brushing up than just the two special move members. Can you tell what else would be needed for a modern LinkedList<> type? Write a quick program that demonstrates the newly added moving capabilities.
Exercise 17-3. Now that we’re digging in the code that you created earlier already, what about the two RAII types you created (or should have created) during Exercise 15-5 when wrapping a C-style API to a fictional database management system? If you recall, one managed a database connection and ensured this connection was always timely severed, whereas the other encapsulated a pointer to the result of a database query whose memory had to be deallocated whenever the user was done inspecting this result. Obviously, you do not want these objects to be copied (why not?). Add the appropriate measures to prevent this.
Exercise 17-4. Like any RAII type (just think of std::unique_ptr<> for an example), the two types you worked on in the previous exercise could surely benefit from move members. Modify the solution of the exercise accordingly, including a few extra lines in the main() function, to prove that your solution works. Is what you did earlier in Exercise 17-3 still needed now? Also, Exercise 15-5 made use of a Customer class. Does this type perhaps need move members as well?