C++ Explained: Object initialization and assignment, lvalues and rvalues, copy and move semantics and the copy-and-swap idiom
In this article I’m going to try and clear up some fundamental C++ topics which are a common source of confusion for beginning and intermediate C++ programmers, and programmers coming to C++ from other languages such as C# or Java:
- the difference between initialization and assignment, which uses of the “=” operator trigger initialization and which trigger assignment, and how to implement them correctly in your own classes
- the meaning of lvalues, rvalues and rvalue references and how to spot which is which in your programs
- an introduction to C++11 move semantics
- when copy and move constructors and assignment operators are called in your classes, and how to implement them
- reducing the amount of duplicated and error-prone code in constructor and assignment operator implementations by using the so-called copy-and-swap idiom
I won’t be dealing with other initialization or assignment topics here (eg. of in-built or POD (plain-old-data) types, and type conversions), only the initialization and assignment of class objects. It is assumed you understand the following concepts (but not necessarily how to implement them):
- constructors and copy constructors
- assignment operator overloading (&operator=)
In the examples below, I will show how to manipulate a class containing a single resource in the form of a std::unique_ptr, which is a single-ownership smart pointer which comes as part of the C++11 standard library, however you can replace this with any resource type which shouldn’t be copied in a bit-wise (shallow copy) manner, eg. any raw pointer or resource handle whose targets should only be freed once no matter how many pointers are held to the resource across object instances, or whose target should be copied if the object instance is copied.
Defining the class
Our class will receive a pointer to a string in its constructor, allocate memory to make a copy of this string, copy it and store a pointer to the copy in a std::unique_ptr. This will be our resource.
The final goal will be to copy the string resource into a new allocated block of memory when an instance of the class is copied (so that when the original object is destructed, the string in the copied object is preserved), and to allow ownership of the resource to be transferred to a different object when the original object is “moved” (I will explain C++11 move semantics below).
A good reason to use std::unique_ptr instead of a raw pointer is that only one object can own (control memory management of) the resource at a time, which prevents accidental copying of the pointer, leading to the possibility that the pointer is freed (delete
‘d) more than once when multiple objects pointing to the same resource are destructed. The copy constructor and copy assignment operator of std::unique_ptr are declared private, which prevents code that might copy the pointer (the std::unique_ptr object) from compiling. This leaves you with only the two desired alternatives: copy the resource when the object is copied (make a new copy of the resource in a different memory block, and therefore a different pointer), or transfer ownership of the pointer to the new object.
The initial definition of the class is:
#include <iostream> #include <memory> class ResourceClass { private: std::unique_ptr<char []> resource; public: // Default constructor ResourceClass() {} // One-argument constructor ResourceClass(const char *resourceName) { char *data; data = new char[strlen(resourceName) + 1]; strcpy_s(data, strlen(resourceName) + 1, resourceName); resource = std::unique_ptr<char []>(data); // OR: resource.reset(data); } };
(std::unique_ptr is defined in memory
in the C++ standard library)
For now, we don’t define a copy constructor or assignment operator overload; the compiler will generate defaults for us. The defaults attempt to copy the object by assigning each member of the new object with the corresponding member of the object being copied, ie. it applies something like memberOfThisObject = memberOfCopiedObject
for each member.
With only the constructor we have supplied so far, we can write code like this:
ResourceClass LoadResource(const char *resourceName) { return ResourceClass(resourceName); } int main() { ResourceClass resource1("FirstResource"); ResourceClass resource2 = ResourceClass("SecondResource"); // Equivalent in this case: // ResourceClass resource2(ResourceClass("SecondResource")); ResourceClass resource3 = LoadResource("ThirdResource"); }
The compiler processes the creation of these three objects as follows:
resource1 is the simplest case where we supply the resource argument to the constructor and the object is constructed in the normal, expected way
resource2 is created in the same way as resource1. Although it may appear from the syntax that a temporary object is created and then copied into resource2, this is not the case. Modern optimizing compilers are smart enough to elide (remove or optimize away) the temporary copy and instead initialize resource2 directly. As a result, the copy constructor or copy assignment operator is not called, only the standard constructor as with resource1.
resource3 uses a factory method to create the object. Again, it appears that returning a new object instance from LoadResource creates a temporary object, but once again the compiler elides the copy and initializes resource3 directly as the object created in LoadResource. Only the standard constructor is used.
The difference between initialization and assignment
- When you declare an object (with its type) and provide an expression to give the object an initial value, this is initialization.
- When you change the value of an existing object (and in fact, in all other cases besides the above), this is assignment.
Initialization:
SomeClass object; SomeClass object(/* constructor arguments */); SomeClass object = /* argument to one-argument constructor which does not invoke type conversion */; SomeClass object = anotherObjectOfSameType; SomeClass object = /* temporary object of same type */;
For the example class above:
ResourceClass object; ResourceClass object("Resource Name"); ResourceClass object = "Resource Name"; // matches type of one-argument constructor, no type conversion ResourceClass someResource; ResourceClass otherResource = someResource; ResourceClass object = ResourceClass("foo"); ResourceClass object = LoadResource("foo");
Assignment:
SomeClass object; // initialize object with default constructor object = SomeClass(/* constructor arguments */); object = anotherObject;
When you initialize an object, the standard constructor, copy constructor, or in C++11 move cosntructor is called.
When you assign to an object, the copy assignment operator, or in C++11 move assignment operator is called. If a temporary object is created on the right-hand side of the expression, the standard constructor for that object is called to initialize the temporary object, then the copy assignment operator (or move assignment operator) is called on the object being assigned to, with the temporary object as the argument, ie.
someObject = anotherObject; // calls copy assignment operator on someObject someObject = SomeClass(/* arguments */); // calls standard constructor on temporary object, then copy assignment operator on someObject
Copy initialization
If you initialize an object with another object, this is called copy initialization, and is the only case in which your object’s copy constructor is called:
SomeClass object = otherObject;
Note that the copy constructor is not called here:
object = otherObject;
because this is assignment, not initialization.
Implementing copy initialization and (copy) assignment
Copy initialization is handled by a copy constructor, and assignment is handled by overloading the assignment operator (=). A naive implementation for our class might look like this:
// Copy constructor ResourceClass(ResourceClass const &r) { char *data; data = new char[strlen(r.resource.get()) + 1]; strcpy_s(data, strlen(r.resource.get()) + 1, r.resource.get()); resource = std::unique_ptr<char []>(data); // OR: resource.reset(data); }
You will notice this code is very similar to the standard constructor we defined earlier, except that we get the existing resource pointer from the object being copied (r) and copy the string from it, rather than copying the string supplied in the argument as we did in the standard constructor.
// Copy assignment operator ResourceClass &operator=(ResourceClass const &r) { if (&r != this) { char *data; data = new char[strlen(r.resource.get()) + 1]; strcpy_s(data, strlen(r.resource.get()) + 1, r.resource.get()); resource = std::unique_ptr<char []>(data); // OR: resource.reset(data); } return *this; }
Assignment is a little more tricky than initialization, because what we are essentially doing is destructing the existing object and then re-constructing it with new values. In a more complex class, you might need to free various resources and then re-allocate them using copies of the resources from the object being copied. std::unique_ptr makes our life easy here, because assigning a new instance of std::unique_ptr to an existing std::unique_ptr object with =
as above (or by using reset()
) first frees the old pointer, so releasing the resource is handled for us automatically (this is the same reason our class doesn’t need a destructor – when std::unique_ptr goes out of scope as our object goes out of scope, std::unique_ptr‘s destructor is called and the resource is freed automatically).
The condition if (&r != this) checks for the usually rare case of self-assignment, in which case we want to do nothing. Indeed, if we try to destroy and re-create resources with the same source and target object, we’re almost certainly going to cause problems, so we want to avoid this. This does impose a very small and usually pointless performance penalty, and below we’ll discuss a way to avoid this check altogether with the copy-and-swap idiom.
Let’s look at what happens now:
Resource resource1("Resource1"); Resource resource2; resource2 = resource1;
This invokes the copy assignment operator to copy resource1
into resource2
. Since a new copy of the resource pointed to by the std::unique_ptr
is made, the resource pointer in resource1
remains valid.
Resource resource3 = resource1;
This invokes the copy constructor (because we are using initialization, not assignment) and creates a new copy of the resource in the same fashion.
lvalues & rvalues
Although the concept of lvalues and rvalues has always existed in C++, it is really with the advent of C++11 that it has become particularly important to understand it. An lvalue is a historical abbrieviation for locator value, and rvalue essentially means “everything that isn’t an lvalue”. Simply put, an lvalue is a concrete variable or object which has non-temporary memory allocated to it, and an rvalue is a temporary expression or object. For example:
someVariable // lvalue 4 // rvalue SomeClass object; object = SomeClass(/* arguments *); // object is an lvalue, the expression on the right is an rvalue int a; a = 10; // a is an lvalue, 10 is an rvalue
When we talk about references in C++, we are really talking about lvalue references:
int a = 4; int &aRef = a; // aRef is an lvalue reference
Notice that lvalue references must always point to an lvalue (unlike raw pointers which don’t have to point to anything specific). They cannot point to an rvalue:
int &ref = 123; // invalid, 123 is an rvalue
This is why you must always initialize a reference when it is declared (or in the initializer list of a class constructor): it must always point to an lvalue. If the reference wasn’t initialized, it would not point to anything.
Once a reference has been initialized, you cannot change the lvalue it points to (references). If you use the assignment operator with the reference, you change the lvalue it references:
int a = 4; int &aRef = a; aRef = 5; // a now contains 5
This is why references can be used on the left-hand side of an assignment statement.
C++11 move semantics: rvalue references
Copy assignment in earlier versions of C++ is often a complete waste of memory and processor time. Consider a class which allocates a lot of memory and performs a lot of work in the constructor:
class SomeClass { private: int *foo; public: SomeClass() : foo(nullptr) {} SomeClass(int increment) { foo = new int[1000000]; for (int i = 0; i < 1000000; i++) foo[i] = i + increment; } ~SomeClass() { if (foo) delete [] foo; } SomeClass &operator=(SomeClass const &c) { if (&c != this) { delete [] foo; foo = new int[1000000]; for (int i = 0; i < 1000000; i++) foo[i] = c.foo[i]; } return *this; } }; int main() { SomeClass o; o = SomeClass(5); }
Wow, this is horrible. When o
is assigned to, a temporary object (an rvalue) is created, allocating 1 million int
s, then the whole object is copied to o
. In this example, it is easily avoidable by simply changing the code to:
SomeClass o(5); // or: SomeClass o = SomeClass(5);
But if o
is a class member, it cannot be initialized directly because C++ forbids non-const
members from being directly initialized when they are declared, so the copy becomes unavoidable.
Since we don’t need the temporary object except for the purposes of initializing o
, what we would really like to do is just copy the int
pointer in foo
, and prevent the temporary object from freeing the allocated memory when it is destructed. C++11 makes this possible by introducing the concepts of rvalue references, the move constructor and the move assignment operator.
When you initialize or assign to an object using a temporary object (an rvalue), C++11 looks to see if you have defined a move constructor or move assignment operator in your class. If you have, the temporary object is passed to it as a modifiable (non-const
) rvalue reference, allowing you to transfer ownership of resource pointers and handles, and nullify them in the temporary object. Note that the destructor in SomeClass
only frees the int
pointer if it is non-null; this is crucial to making this paradigm work correctly.
We can implement the move constructor and move assignment operator as follows:
SomeClass(SomeClass &&c) { foo = c.foo; c.foo = nullptr; } SomeClass &operator=(SomeClass &&c) { if (&c != this) { foo = c.foo; c.foo = nullptr; } return *this; }
Note the use of the special new syntax &&
to indicate that the variable is an rvalue reference. This is much better! When the temporary object is assigned, we now simply copy the pointer instead of the whole array.
Implementing move initialization and move assignment
Let’s see how this applies to our resource class:
// Move constructor ResourceClass(ResourceClass &&r) { resource = std::move(r.resource); } // Move assignment operator ResourceClass &operator=(ResourceClass &&r) { if (&r != this) { resource = std::move(r.resource); } return *this; }
The standard library introduces a new function std::move
which takes an lvalue and turns it into an rvalue reference. This is very handy for handling objects such as std::unique_ptr: if we had just written resource = r.resource
, an attempt to make a copy of the std::unique_ptr
would be made (and fail because the copy assignment operator in std::unique_ptr is declared private). However, std::unique_ptr
defines a public move assignment operator, which copies the raw pointer into the target std::unique_ptr
and nullifies it in the std::unique_ptr
being copied, essentially transferring ownership of the pointer, which is exactly what we want.
Notice that the process is completely transparent and backwards-compatible: if the object can be copied, and no move assignment operator is defined, then the object will be copied on assignment. If a move assignment operator is defined, the assignment will automatically use that instead. Therefore, you don’t need to modify your main application code besides adding a move constructor and move assignment operator to your existing classes.
Let’s look at what happens now:
ResourceClass resource; resource = ResourceClass("SomeResource");
The expression on the right-hand side is an rvalue. The compiler sees that we have defined a move assignment operator, so it converts the temporary object into an rvalue reference and calls our move assignment operator, which transfers ownership of the std::unique_ptr
to the member in resource
.
Similarly, with the factory method:
ResourceClass resource; resource = LoadResource("SomeResource");
The compiler knows that the return value from LoadResource
is an rvalue, and uses the same logic as above to call resource
‘s move assignment operator.
Moving or transferring ownership of an existing object
Existing objects are lvalues, so initialization or assignment of new objects from them will call the copy constructor or copy assignment operator by default. We can use std::move
to convert the existing objects to rvalue references, forcing invocation of the move constructor or move assignment operator instead:
ResourceClass resource1("Resource1"); ResourceClass resource2("Resource2"); ResourceClass resource3 = std::move(resource1); // Calls move constructor ResourceClass resource4; resource4 = std::move(resource2); // Calls move assignment operator
After this code executes, the resources pointed to in resource1
and resource2
will be null, preventing std::unique_ptr
from freeing the resources when the temporary objects are destroyed.
Unifying assignment operator and the copy-and-swap idiom
Let’s face it: adding 3 constructors and 2 assignment operator overloads to our class – which almost entirely duplicate the same code – is pretty horrible. The copy-and-swap idiom kills not two, not three, but four birds with one stone: it allows us to define only a copy constructor and move constructor, then implement the assignment operators in terms of these; it unifies the two assignment operators into a single overload, it provides exception safety in the case of memory allocation failure (for example), and it mitigates the need for a self-assignment check.
There are various implementations and I shall demonstrate the most modern C++11 version here. The principle is that the implementation of the assignment operator will receive the object being assigned from by value instead of by reference, which causes the creation of a temporary object which is local in scope to the assignment operator function. We then swap the contents of the object being assigned to with this local temporary object. When the assignment operator function returns, the temporary object goes out of scope and is destructed, but since have swapped everything with the contents of the existing object, it is the values that were in the existing object which are destructed. Since we are over-writing all of those values with new values from the temporary object, this is exactly what we want!
We delete our existing two assignment operator overloads and replace them with:
ResourceClass &operator=(ResourceClass r) { r.swap(*this); return *this; } private: void swap(ResourceClass &r) throw() { std::swap(this->resource, r.resource); }
In swap()
, you will essentially implement calls to std::swap
on all the members in your class.
The true genius of this method is what happens when the passed-by-value object to be assigned from (the source object) is created as a temporary object in the assignment operator function. There are three possibilities:
- The source object was an lvalue: the source object’s copy constructor will be called to make a copy and the overload will behave as a copy assignment operator.
- The source object was an rvalue: nothing is called; the compiler elides the temporary copy and passes the object by value; the overload will behave as a move assignment operator.
- The source object was an rvalue the source object’s move constructor will be called and the overload will behave as a move assignment operator.
Additionally, as mentioned earlier, because the object is passed to the unifying assignment operator by value, we no longer need to check for self-assignment.
Lvalue example:
ResourceClass resource("ResourceToCopy"); ResourceClass copiedResource; copiedResource = resource;
Intended behaviour: copy the resource in resource
to copiedResource
so that each points to its own copy of the resource. The resource in resource
should remain valid afterwards.
Here is what happens:
resource
is initialized with the standard constructor (thestd::unique_ptr
points to"ResourceToCopy"
)copiedResource
is initialized with the default constructor (thestd::unique_ptr
doesn’t point to anything)resource
is copied intor
using the copy constructor when the unifying assignment operator receivesresource
by value; a new copy of the string resource is made and is pointed to by a new instance ofstd::unique_ptr
- the contents of
copiedResource
andr
are swapped (copiedResource
now has ownership of thestd::unique_ptr
pointing to"ResourceToCopy"
, andr
has ownership of nothing becausecopiedResource
never pointed to a resource originally) r
goes out of scope and is destructed – no resource is destroyed because it is a copy and not the original ofresource
, and the resource it pointed to was replaced by the null resource fromcopiedResource
End result: copiedResource
controls ownership of a new std::unique_ptr
pointing to a new copy of "ResourceToCopy"
and resource
remains unmodified. When resource
or copiedResource
are later destructed, the resource in the other object will not be affected since a copy has been made.
Rvalue example:
ResourceClass resource("ResourceToReplace"); resource = ResourceClass("ResourceToMove");
Intended behaviour: move (transfer ownership of the resource in) to resource
, freeing the resource originally pointed to by resource
.
Here is what happens:
resource
 is initialized with the standard constructor (theÂstd::unique_ptr
 points toÂ"ResourceToReplace"
)- a temporary is created with the standard constructor (theÂ
std::unique_ptr
 points toÂ"ResourceToMove"
) - the temporary is passed to the unifying assignment operator directly by value (no copy is made, the compiler elides it)
- the contents ofÂ
resource
 andÂr
 are swapped (resource
 now has ownership of theÂstd::unique_ptr
 pointing toÂ"ResourceToMove"
, andÂr
 has ownership of theÂstd::unique_ptr
 pointing toÂ"ResourceToReplace"
) r
 goes out of scope and is destructed – theÂ"ResourceToReplace"
 resource is destroyed- the temporary in the calling function goes out of scope and is destructed – nothing happens because the pointer is null; it has already been destructed
End result:Â resource
 now controls ownership of the std::unique_ptr
 pointing to "ResourceToMove"
, and the "ResourceToReplace"
 pointer has been freed
Note that while the behaviour is different to when the copy-and-swap idiom is not used (where the temporary will be moved if the object has a move assignment operator, but copied if it doesn’t have one but does have a copy assignment operator), the end result is the same.
Rvalue reference example:
ResourceClass resource("ResourceToMove"); ResourceClass newResource("ResourceToReplace"); newResource = std::move(resource);
Intended behaviour: move (transfer ownership of the resource in) resource
to newResource
, freeing the resource originally pointed to by newResource
.
Here is what happens:
resource
is initialized with the standard constructor (thestd::unique_ptr
points to"ResourceToMove"
)newResource
is initialized with the standard constructor (thestd::unique_ptr
points to"ResourceToReplace"
)resource
is converted to an rvalue reference and a copy of the object (r
) is made using the move constructor when the unifying assignment operator receivesresource
by value; ownership of the pointer owning"ResourceToMove"
has been transferred fromresource
tor
- the contents of
newResource
andr
are swapped (newResource
now has ownership of thestd::unique_ptr
pointing to"ResourceToMove"
, andr
has ownership of thestd::unique_ptr
pointing to"ResourceToReplace"
) r
goes out of scope and is destructed – the"ResourceToReplace"
resource is destroyed
End result: newResource
now controls ownership of the std::unique_ptr
pointing to "ResourceToMove"
, the "ResourceToReplace"
pointer has been freed and resource
no longer has ownership of any pointer/resource. When resource
is later destructed, no resource will be freed as ownership has been transferred to newResource
.
Bringing it all together
Below are two concrete example programs, one using the copy-and-swap idiom and one using the normal assignment operators. Run these programs to prove that the resource copies and moves work as expected, and step through them with your debugger to confirm which constructors and assignment operator overloads are called for each initialization and assignment. Note that the contents of main() in both examples is identical, but the comments in some places are different to highlight the different behaviour when the copy-and-swap idiom is used.
Without copy-and-swap idiom:
#include <iostream> #include <memory> class ResourceClass { private: std::unique_ptr<char []> resource; public: // Default constructor ResourceClass() {} // One-argument constructor ResourceClass(const char *resourceName) { char *data; data = new char[strlen(resourceName) + 1]; strcpy_s(data, strlen(resourceName) + 1, resourceName); resource = std::unique_ptr<char []>(data); // OR: resource.reset(data); } // Copy constructor ResourceClass(ResourceClass const &r) { char *data; data = new char[strlen(r.resource.get()) + 1]; strcpy_s(data, strlen(r.resource.get()) + 1, r.resource.get()); resource = std::unique_ptr<char []>(data); // OR: resource.reset(data); } // Copy assignment operator ResourceClass &operator=(ResourceClass const &r) { if (&r != this) { char *data; data = new char[strlen(r.resource.get()) + 1]; strcpy_s(data, strlen(r.resource.get()) + 1, r.resource.get()); resource = std::unique_ptr<char []>(data); // OR: resource.reset(data); } return *this; } // Move constructor ResourceClass(ResourceClass &&r) { resource = std::move(r.resource); } // Move assignment operator ResourceClass &operator=(ResourceClass &&r) { if (&r != this) { resource = std::move(r.resource); } return *this; } void print() { if (resource) std::cout << resource.get() << std::endl; else std::cout << "Resource not set" << std::endl; } }; ResourceClass LoadResource(const char *resourceName) { return ResourceClass(resourceName); } int main() { // Uses one-argument constructor ResourceClass resource1("FirstResource"); // Uses one-argument constructor as above // Direct initialization due to no type conversion // Does NOT use copy constructor or assignment operator // Temporary copy is elided by compiler ResourceClass resource2 = ResourceClass("SecondResource"); // Uses one-argument constructor // Direct initialization due to no type conversion ResourceClass resource2a = "SecondResourceA"; // Equivalent in this case: // ResourceClass resource2(ResourceClass("SecondResource")); // Uses one-argument constructor as above // Temporary copy is elided by compiler ResourceClass resource3 = LoadResource("ThirdResource"); // Equivalent in this case: // ResourceClass resource3(LoadResource("ThirdResource")); // Initialized with default constructor // Rvalue is created with one-argument constructor // and assigned to resource4 with the move assignment operator // if one exists, or the copy assignment operator if not // If the copy/move assignment operator wasn't defined, the compiler prevents // std::unique_ptr from being copied because its own assignment operator // is declared private to prevent copying, and you get a compiler error: // 'std::unique_ptr<_Ty>::operator =' : cannot access private member declared in class 'std::unique_ptr<_Ty>' ResourceClass resource4; resource4 = ResourceClass("FourthResource"); /* Example: std::unique_ptr<int> first(new int); std::unique_ptr<int> second = first; // Fails, copy constructor is private std::unique_ptr<int> third(first); // Fails, copy constructor is private std::unique_ptr<int> fourth; fourth = first; // Fails, assignment operator is private */ // Initialized with default constructor // Rvalue is created with one-argument constructor // and assigned to resource5 with the move assignment operator // if one exists, or the copy assignment operator if not ResourceClass resource5; resource5 = LoadResource("FifthResource"); // See what we've got resource1.print(); resource2.print(); resource3.print(); resource4.print(); resource5.print(); std::cout << std::endl; // Replace resource3 with resource2 // This uses the copy assignment operator and copies the resource; resource2 is still valid resource3 = resource2; // See what we've got resource1.print(); resource2.print(); resource3.print(); resource4.print(); resource5.print(); std::cout << std::endl; // Move resource4 to resource1 // This uses the move assignment operator and moves the resource pointer only; // resource4 will no longer manage the resource resource1 = std::move(resource4); // See what we've got resource1.print(); resource2.print(); resource3.print(); resource4.print(); resource5.print(); std::cout << std::endl; // Copies the resource in resource1 to resource6; resource1 is still valid // Uses the copy constructor ResourceClass resource6 = resource1; // Equivalent in this case: // ResourceClass resource6(resource1); // See what we've got resource1.print(); resource2.print(); resource3.print(); resource4.print(); resource5.print(); resource6.print(); std::cout << std::endl; // Moves the resource in resource6 to resource7; // the resource is now managed by resource7 // Uses the move constructor ResourceClass resource7 = std::move(resource6); // See what we've got resource1.print(); resource2.print(); resource3.print(); resource4.print(); resource5.print(); resource6.print(); resource7.print(); std::cout << std::endl; while(true); }
With copy-and-swap idiom:
#include <iostream> #include <memory> class ResourceClass { private: std::unique_ptr<char []> resource; public: // Default constructor ResourceClass() {} ~ResourceClass() {} // One-argument constructor ResourceClass(const char *resourceName) { char *data; data = new char[strlen(resourceName) + 1]; strcpy_s(data, strlen(resourceName) + 1, resourceName); resource = std::unique_ptr<char []>(data); // OR: resource.reset(data); } // Copy constructor ResourceClass(ResourceClass const &r) { char *data; data = new char[strlen(r.resource.get()) + 1]; strcpy_s(data, strlen(r.resource.get()) + 1, r.resource.get()); resource = std::unique_ptr<char []>(data); // OR: resource.reset(data); } // Move constructor ResourceClass(ResourceClass &&r) { resource = std::move(r.resource); } // Unifying assignment operator // Replaces the copy assignment operator and move assignment operator // NOTE: The source object is passed by value. This is important! // If the source is an lvalue, the copy constructor will be called // If the source is an rvalue, nothing will be called // If the source is an rvalue reference, the move constructor will be called ResourceClass &operator=(ResourceClass r) { r.swap(*this); return *this; } void print() { if (resource) std::cout << resource.get() << std::endl; else std::cout << "Resource not set" << std::endl; } private: void swap(ResourceClass &r) throw() { std::swap(this->resource, r.resource); } }; ResourceClass LoadResource(const char *resourceName) { return ResourceClass(resourceName); } int main() { // Uses one-argument constructor ResourceClass resource1("FirstResource"); // Uses one-argument constructor as above // Direct initialization due to no type conversion // Does NOT use copy constructor or assignment operator // Temporary copy is elided by compiler ResourceClass resource2 = ResourceClass("SecondResource"); // Uses one-argument constructor // Direct initialization due to no type conversion ResourceClass resource2a = "SecondResourceA"; // Equivalent in this case: // ResourceClass resource2(ResourceClass("SecondResource")); // Uses one-argument constructor as above // Temporary copy is elided by compiler ResourceClass resource3 = LoadResource("ThirdResource"); // Equivalent in this case: // ResourceClass resource3(LoadResource("ThirdResource")); // Initialized with default constructor // Rvalue is created with one-argument constructor // and assigned to resource4 with the unifying assignment operator // as a move assignment since the temporary on the right-hand side // of the expression is an rvalue. ResourceClass resource4; resource4 = ResourceClass("FourthResource"); // Initialized with default constructor // Rvalue is created with one-argument constructor // and assigned to resource5 with the unifying assignment operator // as a move assignment since the temporary on the right-hand side // of the expression is an rvalue. ResourceClass resource5; resource5 = LoadResource("FifthResource"); // See what we've got resource1.print(); resource2.print(); resource3.print(); resource4.print(); resource5.print(); std::cout << std::endl; // Replace resource3 with resource2 // This uses the unifying assignment operator // as a copy assignment since the right-hand side is an lvalue; // resource2 is still valid afterwards resource3 = resource2; // See what we've got resource1.print(); resource2.print(); resource3.print(); resource4.print(); resource5.print(); std::cout << std::endl; // Move resource4 to resource1 // This uses the unifying assignment operator // as a move assignment since the right-hand side is an rvalue reference; // resource4 will no longer manage the resource resource1 = std::move(resource4); // See what we've got resource1.print(); resource2.print(); resource3.print(); resource4.print(); resource5.print(); std::cout << std::endl; // Copies the resource in resource1 to resource6; resource1 is still valid // Uses the copy constructor ResourceClass resource6 = resource1; // Equivalent in this case: // ResourceClass resource6(resource1); // See what we've got resource1.print(); resource2.print(); resource3.print(); resource4.print(); resource5.print(); resource6.print(); std::cout << std::endl; // Moves the resource in resource6 to resource7; // the resource is now managed by resource7 // Uses the move constructor ResourceClass resource7 = std::move(resource6); // See what we've got resource1.print(); resource2.print(); resource3.print(); resource4.print(); resource5.print(); resource6.print(); resource7.print(); std::cout << std::endl; while(true); }
Both programs produce the same output, of course:
FirstResource SecondResource ThirdResource FourthResource FifthResource FirstResource SecondResource SecondResource FourthResource FifthResource FourthResource SecondResource SecondResource Resource not set FifthResource FourthResource SecondResource SecondResource Resource not set FifthResource FourthResource FourthResource SecondResource SecondResource Resource not set FifthResource Resource not set FourthResource
I hope you found this article useful. I’ll finish with some references to other great related articles where you can find more intricate details. Please leave comments and feedback below!
References
Move semantics and rvalue references in C++11
The new C++ 11 rvalue reference && and why you should start using it
Understanding lvalues and rvalues in C and C++
Great summary, Katy! I think move semantics will continue to feel a bit like black magic to me until I actually write production code that uses that feature of the language. So far, none of my codebases are using C++ 11 stuff, even though our compilers support it. Here’s hoping!
Thanks for the great post. Keep it up, I’ve read many different dev blogs, especially on C++, it’s a rarety to see an author to delve into fundamentals and broadly reason. So please keep it up. I think it’s a good sign I’m second to write a comment, as basically what questions I’ve come up with, have been answered in the course of reading. If you’d also write some more on design patterns, that would be just awesome.
btw,
is preferred now to
, regarding your
method.
Terrific article though the depth of the subject matter makes it one for experienced developers only. I didn’t scrutinize it in exhaustive detail but did read it from beginning to end and found no glaring errors. It appears to be very accurate and definitely well written (!), though for efficiency you really shouldn’t call “strlen()” more than once on the same string (ok, I’m nitpicking since it has nothing to do with the article and I’m sure you must know this). It’s not an exhaustive treatise on all the issues either (first-timers may be confused that an rvalue reference turns into an lvalue reference once a parameter name has been assigned to it), but I wouldn’t expect it. One could go on ad nauseam about the countless issues involved (it’s a blog, not a book), but overall it’s a *great* explanation, really one of the clearest and most comprehensive write-ups I’ve seen on the web (and clearly demonstrating your deep understanding of the issues). Kudos for a great job!
Hey, thanks so much for both the compliments and the constructive criticism, I welcome it. As you said it’s not possible to go over everything in a blog post, and my programming skills certainly aren’t perfect so forgetful things like the multiple calls to strlen() will creep in so I appreciate the reminder đŸ™‚ Obviously I was focusing more on the whole issue of rvalue references and such here.
Anyway, thanks for the feedback!
I have just started to learn programming and my knowledge is so limited, but this article is really easy to understand. I was looking for explanations for copy constructor / shallow copy / deep copy and found this article. These topics seemed to me very complex and advanced, and when I looked up on the Interned I only found materials explaining it in complicated ways and I was so confused. Thank you for kindly explaining the topics, this article is so beginner friendly !!