Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Implementing Move Semantics
Write constructors and assignment operators that steal resources from temporaries.
Move constructors and move assignment
In the first lesson of this chapter, we examined std::auto_ptr, discussed the motivation for move semantics, and explored problems that arise when functions designed for copy semantics (copy constructors and copy assignment operators) are repurposed to implement move semantics.
In this lesson, we'll examine how C++11 resolves these issues through move constructors and move assignment.
Recapping copy constructors and copy assignment
First, let's review copy semantics.
Copy constructors initialize a class by making a copy of an object of the same class. Copy assignment copies one class object to another existing class object. By default, C++ provides a copy constructor and copy assignment operator if not explicitly defined. These compiler-provided functions perform shallow copies, which can cause problems for classes managing dynamic memory. Such classes should override these functions to perform deep copies.
Returning to our smart pointer example from the first lesson, let's look at a version implementing a copy constructor and copy assignment operator that perform deep copies, with a program demonstrating their use:
#include <iostream>
template<typename T>
class SmartPtr3
{
T* m_ptr{};
public:
SmartPtr3(T* ptr = nullptr)
: m_ptr{ptr}
{
}
~SmartPtr3()
{
delete m_ptr;
}
// Copy constructor
// Perform deep copy of other.m_ptr to m_ptr
SmartPtr3(const SmartPtr3& other)
{
m_ptr = new T;
*m_ptr = *other.m_ptr;
}
// Copy assignment
// Perform deep copy of other.m_ptr to m_ptr
SmartPtr3& operator=(const SmartPtr3& other)
{
// Self-assignment check
if (&other == this)
return *this;
// Release current resource
delete m_ptr;
// Copy the resource
m_ptr = new T;
*m_ptr = *other.m_ptr;
return *this;
}
T& operator*() const { return *m_ptr; }
T* operator->() const { return m_ptr; }
bool isNull() const { return m_ptr == nullptr; }
};
class FileHandle
{
public:
FileHandle() { std::cout << "File opened\n"; }
~FileHandle() { std::cout << "File closed\n"; }
};
SmartPtr3<FileHandle> openFile()
{
SmartPtr3<FileHandle> file{new FileHandle};
return file; // this return value invokes the copy constructor
}
int main()
{
SmartPtr3<FileHandle> mainFile;
mainFile = openFile(); // this assignment invokes the copy assignment
return 0;
}
In this program, we use function openFile() to create a smart pointer encapsulating a file handle, which is returned to main(). Function main() then assigns it to an existing SmartPtr3 object.
When run, this prints:
File opened
File opened
File closed
File opened
File closed
File closed
(Note: You may see only 4 outputs if your compiler elides the return value from openFile())
That's excessive resource creation and destruction for such a simple program! What's happening?
There are 6 key steps in this program (one for each printed message):
- Inside openFile(), local variable file is created and initialized with a dynamically allocated FileHandle, causing the first "File opened".
- File is returned to main() by value. We return by value because file is local -- it can't be returned by address or reference since file is destroyed when openFile() ends. File is copy constructed into a temporary object. Since our copy constructor performs a deep copy, a new FileHandle is allocated, causing the second "File opened".
- File goes out of scope, destroying the originally created FileHandle, causing the first "File closed".
- The temporary object is assigned to mainFile via copy assignment. Since our copy assignment also performs a deep copy, yet another FileHandle is allocated, causing another "File opened".
- The assignment expression ends, and the temporary goes out of scope and is destroyed, causing a "File closed".
- At main()'s end, mainFile goes out of scope, and our final "File closed" is displayed.
In summary, calling the copy constructor once to copy file to a temporary, and copy assignment once to copy the temporary into mainFile, results in allocating and destroying 3 separate objects total.
Inefficient, but at least it doesn't crash!
However, with move semantics, we can do better.
Move constructors and move assignment
C++11 defines two new functions for move semantics: a move constructor and a move assignment operator. While the goal of the copy constructor and copy assignment is to make a copy of one object to another, the goal of the move constructor and move assignment is to move ownership of resources from one object to another (typically much cheaper than copying).
Defining move constructors and move assignment works analogously to their copy counterparts. However, while copy versions take a const l-value reference parameter (binding to just about anything), move versions use non-const rvalue reference parameters (binding only to rvalues).
Here's the same SmartPtr3 class with move constructor and move assignment added. We've kept the deep-copying copy constructor and copy assignment for comparison:
#include <iostream>
template<typename T>
class SmartPtr4
{
T* m_ptr{};
public:
SmartPtr4(T* ptr = nullptr)
: m_ptr{ptr}
{
}
~SmartPtr4()
{
delete m_ptr;
}
// Copy constructor
// Perform deep copy of other.m_ptr to m_ptr
SmartPtr4(const SmartPtr4& other)
{
m_ptr = new T;
*m_ptr = *other.m_ptr;
}
// Move constructor
// Transfer ownership of other.m_ptr to m_ptr
SmartPtr4(SmartPtr4&& other) noexcept
: m_ptr{other.m_ptr}
{
other.m_ptr = nullptr; // we'll discuss this line below
}
// Copy assignment
// Perform deep copy of other.m_ptr to m_ptr
SmartPtr4& operator=(const SmartPtr4& other)
{
// Self-assignment check
if (&other == this)
return *this;
// Release current resource
delete m_ptr;
// Copy the resource
m_ptr = new T;
*m_ptr = *other.m_ptr;
return *this;
}
// Move assignment
// Transfer ownership of other.m_ptr to m_ptr
SmartPtr4& operator=(SmartPtr4&& other) noexcept
{
// Self-assignment check
if (&other == this)
return *this;
// Release current resource
delete m_ptr;
// Transfer ownership of other.m_ptr to m_ptr
m_ptr = other.m_ptr;
other.m_ptr = nullptr; // we'll discuss this line below
return *this;
}
T& operator*() const { return *m_ptr; }
T* operator->() const { return m_ptr; }
bool isNull() const { return m_ptr == nullptr; }
};
class FileHandle
{
public:
FileHandle() { std::cout << "File opened\n"; }
~FileHandle() { std::cout << "File closed\n"; }
};
SmartPtr4<FileHandle> openFile()
{
SmartPtr4<FileHandle> file{new FileHandle};
return file; // this return value invokes the move constructor
}
int main()
{
SmartPtr4<FileHandle> mainFile;
mainFile = openFile(); // this assignment invokes the move assignment
return 0;
}
The move constructor and move assignment are straightforward. Instead of deep copying the source object (other) into the destination object (the implicit object), we simply move (steal) the source object's resources. This involves shallow copying the source pointer into the implicit object, then setting the source pointer to null.
When run, this prints:
File opened
File closed
Much better!
The program flow is exactly the same. However, instead of calling copy constructor and copy assignment operators, this program calls move constructor and move assignment operators. Looking deeper:
- Inside openFile(), local variable file is created and initialized with a dynamically allocated FileHandle, causing the first "File opened".
- File is returned to main() by value. File is move constructed into a temporary object, transferring the dynamically created object stored in file to the temporary. We'll explain why this happens below.
- File goes out of scope. Since file no longer manages a pointer (it was moved to the temporary), nothing interesting happens.
- The temporary object is move assigned to mainFile. This transfers the dynamically created object stored in the temporary to mainFile.
- The assignment expression ends, and the temporary goes out of scope and is destroyed. However, since the temporary no longer manages a pointer (it was moved to mainFile), nothing interesting happens.
- At main()'s end, mainFile goes out of scope, and our final "File closed" is displayed.
Instead of copying our FileHandle twice (once for copy constructor and once for copy assignment), we transfer it twice. This is more efficient, as FileHandle is only constructed and destroyed once instead of three times.
Advanced note: Move constructors and move assignment should be marked as noexcept. This tells the compiler these functions won't throw exceptions, allowing standard library containers to optimize certain operations.
When are move constructors and move assignment called?
The move constructor and move assignment are called when those functions are defined and the argument for construction or assignment is an rvalue. Most typically, this rvalue will be a literal or temporary value.
The copy constructor and copy assignment are used otherwise (when the argument is an lvalue, or when the argument is an rvalue but move constructor/assignment functions aren't defined).
Implicit move constructor and move assignment operator
The compiler creates an implicit move constructor and move assignment operator if all of the following are true:
- There are no user-declared copy constructors or copy assignment operators.
- There are no user-declared move constructors or move assignment operators.
- There is no user-declared destructor.
These functions perform a memberwise move, which behaves as follows:
- If a member has a move constructor or move assignment (as appropriate), it will be invoked.
- Otherwise, the member will be copied.
Notably, this means pointers will be copied, not moved!
The implicit move constructor and move assignment will copy pointers, not move them. To move a pointer member, define the move constructor and move assignment yourself.
Key insights behind move semantics
You now have sufficient context to understand move semantics.
When constructing an object or performing an assignment where the argument is an l-value, the only reasonable action is copying the l-value. We can't assume it's safe to alter the l-value, as it may be used again later. If we have expression "a = b" (where b is an lvalue), we wouldn't expect b to change.
However, when constructing an object or performing an assignment where the argument is an r-value, we know that r-value is just a temporary object. Instead of copying it (expensive), we can simply transfer its resources (cheap) to the object we're constructing or assigning. This is safe because the temporary will be destroyed at expression end, so we know it will never be used again!
C++11, through r-value references, provides the ability to offer different behaviors when the argument is an r-value versus an l-value, enabling smarter and more efficient decisions about object behavior.
Move semantics is an optimization opportunity. When an argument is an r-value (temporary), we can transfer its resources instead of copying them, knowing the temporary will be destroyed anyway.
Move functions should always leave both objects in a valid state
In the above examples, both move constructor and move assignment set other.m_ptr to nullptr. This may seem unnecessary -- after all, if other is a temporary r-value, why bother with "cleanup" if parameter other will be destroyed anyway?
The answer is simple: When other goes out of scope, the destructor for other will be called, and other.m_ptr will be deleted. If at that point other.m_ptr is still pointing to the same object as m_ptr, then m_ptr will be left as a dangling pointer. When the object containing m_ptr eventually gets used (or destroyed), we'll get undefined behavior.
When implementing move semantics, ensure the moved-from object is left in a valid state so it will destruct properly (without creating undefined behavior).
Automatic l-values returned by value may be moved instead of copied
In the openFile() function of the SmartPtr4 example above, when variable file is returned by value, it is moved instead of copied, even though file is an l-value. The C++ specification has a special rule allowing automatic objects returned from a function by value to be moved even if they are l-values. This makes sense, since file was going to be destroyed at function end anyway! We might as well steal its resources instead of making an expensive and unnecessary copy.
Although the compiler can move l-value return values, in some cases it may be able to do even better by simply eliding the copy altogether (avoiding the need to make a copy or do a move at all). In such a case, neither copy constructor nor move constructor would be called.
Disabling copying
In the SmartPtr4 class above, we left in the copy constructor and assignment operator for comparison. But in move-enabled classes, it's sometimes desirable to delete the copy constructor and copy assignment to ensure copies aren't made. For our SmartPtr class, we don't want to copy our templated object T -- both because it's expensive, and whatever class T is may not even support copying!
Here's a version of SmartPtr supporting move semantics but not copy semantics:
#include <iostream>
template<typename T>
class SmartPtr5
{
T* m_ptr{};
public:
SmartPtr5(T* ptr = nullptr)
: m_ptr{ptr}
{
}
~SmartPtr5()
{
delete m_ptr;
}
// Copy constructor -- no copying allowed!
SmartPtr5(const SmartPtr5& other) = delete;
// Move constructor
// Transfer ownership of other.m_ptr to m_ptr
SmartPtr5(SmartPtr5&& other) noexcept
: m_ptr{other.m_ptr}
{
other.m_ptr = nullptr;
}
// Copy assignment -- no copying allowed!
SmartPtr5& operator=(const SmartPtr5& other) = delete;
// Move assignment
// Transfer ownership of other.m_ptr to m_ptr
SmartPtr5& operator=(SmartPtr5&& other) noexcept
{
// Self-assignment check
if (&other == this)
return *this;
// Release current resource
delete m_ptr;
// Transfer ownership of other.m_ptr to m_ptr
m_ptr = other.m_ptr;
other.m_ptr = nullptr;
return *this;
}
T& operator*() const { return *m_ptr; }
T* operator->() const { return m_ptr; }
bool isNull() const { return m_ptr == nullptr; }
};
If you tried passing a SmartPtr5 l-value to a function by value, the compiler would complain that the copy constructor required to initialize the function parameter has been deleted. This is good, because we should probably be passing SmartPtr5 by const l-value reference anyway!
SmartPtr5 is (finally) a good smart pointer class. The standard library contains a class very similar to this one (that you should use instead), named std::unique_ptr. We'll discuss std::unique_ptr later in this chapter.
Another example
Let's examine another class using dynamic memory: a simple dynamic templated array. This class contains a deep-copying copy constructor and copy assignment operator.
#include <cstddef> // for std::size_t
#include <algorithm> // for std::copy_n
template <typename T>
class Buffer
{
private:
T* m_data{};
int m_size{};
void allocate(int size)
{
m_data = new T[static_cast<std::size_t>(size)];
m_size = size;
}
public:
Buffer(int size)
{
allocate(size);
}
~Buffer()
{
delete[] m_data;
}
// Copy constructor
Buffer(const Buffer& other)
{
allocate(other.m_size);
std::copy_n(other.m_data, m_size, m_data);
}
// Copy assignment
Buffer& operator=(const Buffer& other)
{
if (&other == this)
return *this;
delete[] m_data;
allocate(other.m_size);
std::copy_n(other.m_data, m_size, m_data);
return *this;
}
int getSize() const { return m_size; }
T& operator[](int index) { return m_data[index]; }
const T& operator[](int index) const { return m_data[index]; }
};
Now let's use this class in a program. To show performance with a million integers on the heap, we'll use the Timer class. We'll time how fast our code runs, showing the performance difference between copying and moving.
#include <algorithm> // for std::copy_n
#include <chrono> // for std::chrono functions
#include <iostream>
// Uses the above Buffer class
class Timer
{
private:
using Clock = std::chrono::high_resolution_clock;
using Second = std::chrono::duration<double, std::ratio<1>>;
std::chrono::time_point<Clock> m_start{Clock::now()};
public:
void reset()
{
m_start = Clock::now();
}
double elapsed() const
{
return std::chrono::duration_cast<Second>(Clock::now() - m_start).count();
}
};
// Return a copy of buffer with all values doubled
Buffer<int> processBuffer(const Buffer<int>& buffer)
{
Buffer<int> result(buffer.getSize());
for (int i{0}; i < buffer.getSize(); ++i)
result[i] = buffer[i] * 2;
return result;
}
int main()
{
Timer timer;
Buffer<int> buffer(1000000);
for (int i{0}; i < buffer.getSize(); i++)
buffer[i] = i;
buffer = processBuffer(buffer);
std::cout << timer.elapsed();
}
When tested in release mode, this program executed in 0.00812 seconds. Your results may differ.
Now let's update Buffer by replacing the copy constructor and copy assignment with a move constructor and move assignment, then run the program again:
#include <cstddef> // for std::size_t
template <typename T>
class Buffer
{
private:
T* m_data{};
int m_size{};
void allocate(int size)
{
m_data = new T[static_cast<std::size_t>(size)];
m_size = size;
}
public:
Buffer(int size)
{
allocate(size);
}
~Buffer()
{
delete[] m_data;
}
// Copy constructor
Buffer(const Buffer& other) = delete;
// Copy assignment
Buffer& operator=(const Buffer& other) = delete;
// Move constructor
Buffer(Buffer&& other) noexcept
: m_data{other.m_data}, m_size{other.m_size}
{
other.m_size = 0;
other.m_data = nullptr;
}
// Move assignment
Buffer& operator=(Buffer&& other) noexcept
{
if (&other == this)
return *this;
delete[] m_data;
m_size = other.m_size;
m_data = other.m_data;
other.m_size = 0;
other.m_data = nullptr;
return *this;
}
int getSize() const { return m_size; }
T& operator[](int index) { return m_data[index]; }
const T& operator[](int index) const { return m_data[index]; }
};
#include <iostream>
#include <chrono> // for std::chrono functions
class Timer
{
private:
using Clock = std::chrono::high_resolution_clock;
using Second = std::chrono::duration<double, std::ratio<1>>;
std::chrono::time_point<Clock> m_start{Clock::now()};
public:
void reset()
{
m_start = Clock::now();
}
double elapsed() const
{
return std::chrono::duration_cast<Second>(Clock::now() - m_start).count();
}
};
// Return a copy of buffer with all values doubled
Buffer<int> processBuffer(const Buffer<int>& buffer)
{
Buffer<int> result(buffer.getSize());
for (int i{0}; i < buffer.getSize(); ++i)
result[i] = buffer[i] * 2;
return result;
}
int main()
{
Timer timer;
Buffer<int> buffer(1000000);
for (int i{0}; i < buffer.getSize(); i++)
buffer[i] = i;
buffer = processBuffer(buffer);
std::cout << timer.elapsed();
}
On the same machine, this program executed in 0.0054 seconds.
Comparing runtime: (0.00812 - 0.0054) / 0.00812 * 100 = 33.5% faster!
Deleting the move constructor and move assignment
You can delete the move constructor and move assignment using the = delete syntax in the same way you delete the copy constructor and copy assignment.
#include <iostream>
#include <string>
#include <string_view>
class Account
{
private:
std::string m_username{};
public:
Account(std::string_view username) : m_username{username}
{
}
Account(const Account& other) = delete;
Account& operator=(const Account& other) = delete;
Account(Account&& other) = delete;
Account& operator=(Account&& other) = delete;
const std::string& getUsername() const { return m_username; }
};
int main()
{
Account user1{"Alice"};
user1 = Account{"Bob"}; // error: move assignment deleted
std::cout << user1.getUsername() << '\n';
return 0;
}
If you delete the copy constructor, the compiler will not generate an implicit move constructor (making your objects neither copyable nor movable). Therefore, when deleting the copy constructor, be explicit about desired behavior from your move constructors. Either explicitly delete them (making it clear this is desired behavior), or default them (making the class move-only).
The rule of five states that if the copy constructor, copy assignment, move constructor, move assignment, or destructor are defined or deleted, then each of those functions should be defined or deleted.
While deleting only the move constructor and move assignment may seem like a good idea for a copyable but not movable object, this has the unfortunate consequence of making the class not returnable by value in cases where mandatory copy elision doesn't apply. This happens because a deleted move constructor is still a declared function and is eligible for overload resolution. Return by value will favor a deleted move constructor over a non-deleted copy constructor. This is illustrated by the following program:
#include <iostream>
#include <string>
#include <string_view>
class Account
{
private:
std::string m_username{};
public:
Account(std::string_view username) : m_username{username}
{
}
Account(const Account& other) = default;
Account& operator=(const Account& other) = default;
Account(Account&& other) = delete;
Account& operator=(Account&& other) = delete;
const std::string& getUsername() const { return m_username; }
};
Account createAccount()
{
Account temp{"Charlie"};
return temp; // error: Move constructor was deleted
}
int main()
{
Account user{createAccount()};
std::cout << user.getUsername() << '\n';
return 0;
}
Issues with move semantics and std::swap (Advanced)
Copy and swap also works for move semantics, meaning we can implement our move constructor and move assignment by swapping resources with the object that will be destroyed.
This has two benefits:
- The persistent object now controls the resources previously owned by the dying object (our primary goal).
- The dying object now controls the resources previously owned by the persistent object. When the dying object actually dies, it can perform any required cleanup on those resources.
When you think about swapping, std::swap() usually comes to mind. However, implementing move constructor and move assignment using std::swap() is problematic, as std::swap() calls both the move constructor and move assignment on move-capable objects. This will result in infinite recursion.
You can see this happen in the following example:
#include <iostream>
#include <string>
#include <string_view>
class Account
{
private:
std::string m_username{}; // std::string is move capable
public:
Account(std::string_view username) : m_username{username}
{
}
Account(const Account& other) = delete;
Account& operator=(const Account& other) = delete;
Account(Account&& other) noexcept
{
std::cout << "Move ctor\n";
std::swap(*this, other); // bad!
}
Account& operator=(Account&& other) noexcept
{
std::cout << "Move assign\n";
std::swap(*this, other); // bad!
return *this;
}
const std::string& getUsername() const { return m_username; }
};
int main()
{
Account user1{"Diana"};
user1 = Account{"Eve"}; // invokes move assignment
std::cout << user1.getUsername() << '\n';
return 0;
}
This prints:
Move assign
Move ctor
Move ctor
Move ctor
Move ctor
And continues until stack overflow.
Advanced note: You can implement move constructor and move assignment using your own swap function, as long as your swap member function doesn't call the move constructor or move assignment. Here's how:
#include <iostream>
#include <string>
#include <string_view>
class Account
{
private:
std::string m_username{};
public:
Account(std::string_view username) : m_username{username}
{
}
Account(const Account& other) = delete;
Account& operator=(const Account& other) = delete;
// Create our own swap friend function to swap the members of Account
friend void swap(Account& a, Account& b) noexcept
{
// We avoid recursive calls by invoking std::swap on the std::string member,
// not on Account
std::swap(a.m_username, b.m_username);
}
Account(Account&& other) noexcept
{
std::cout << "Move ctor\n";
swap(*this, other); // Now calling our swap, not std::swap
}
Account& operator=(Account&& other) noexcept
{
std::cout << "Move assign\n";
swap(*this, other); // Now calling our swap, not std::swap
return *this;
}
const std::string& getUsername() const { return m_username; }
};
int main()
{
Account user1{"Fiona"};
user1 = Account{"Grace"}; // invokes move assignment
std::cout << user1.getUsername() << '\n';
return 0;
}
This works as expected and prints:
Move assign
Grace
Summary
Copy semantics inefficiency: Deep copying creates and destroys multiple resource instances when passing or returning objects by value. For classes managing expensive resources, this creates significant overhead.
Move constructor: Takes a non-const r-value reference parameter and transfers ownership of resources from source to destination. Must mark the source's pointers as nullptr after transfer to prevent double deletion. Should be marked noexcept.
Move assignment: Similar to move constructor but must also clean up the destination's existing resources, check for self-assignment, and return *this. Transfers ownership and nullifies source pointers.
When move functions are called: Move constructor and move assignment are invoked when the argument is an r-value (temporary or literal). Otherwise, copy versions are used. The compiler can move l-value return values from functions.
Implicit move functions: The compiler generates implicit move constructor and move assignment only if no user-declared copy/move functions or destructors exist. These perform memberwise moves, which copies pointers rather than moving them.
Move semantics rationale: L-values may be used again, so copying is necessary. R-values are temporaries that will be destroyed, so we can safely transfer their resources instead of copying. This optimization is enabled by r-value references.
Valid state requirement: Move functions must leave both source and destination in valid states. The source must be safe to destruct (typically by setting pointers to nullptr). The destination takes ownership of the resource.
The rule of five: If you define or delete any of the following, define or delete all five: destructor, copy constructor, copy assignment, move constructor, move assignment.
Disabling copy for move-only classes: Delete copy constructor and copy assignment while implementing move versions to create move-only classes. This prevents expensive copies while allowing efficient transfers.
Performance benefits: Moving can be significantly faster than copying for classes managing expensive resources like dynamic memory. The performance gain increases with resource size.
Implement move constructor and move assignment for classes that manage resources. Mark them noexcept. Ensure moved-from objects are left in a valid state that can be safely destroyed.
Implementing Move Semantics - Quiz
Test your understanding of the lesson.
Practice Exercises
Implement a Move-Enabled String Buffer
Create a StringBuffer class that manages dynamic character arrays with both copy and move semantics. The class should demonstrate the performance benefits of move operations over copy operations when transferring ownership of resources.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!