3. Copy and Move Constructors
Regular constructors allow you to invoke some logic and initialize data members when an object is created from a list of arguments. But C++ also has two special constructor types that let you control a situation when an object is created using an instance of the same class type. Those constructors are called copy and move constructors. Let’s have a look.
Copy constructor
A copy constructor is a special member function taking an object of the same type as the first argument, usually by const reference.
ClassName(const ClassName&);
Technically it might have other parameters, but they all have to have default values assigned.
It’s used and called when you create an object using a variable of the same type, to be precise, when you use copy initialization.
Product base { 42, "base product" }; // an initial object
// various forms of initialization, where a copy constructor is called
Product other { base };
Product another(base);
Product oneMore = base;
Product arr[] = { base, other, oneMore };
Implementing a copy constructor might be necessary when your class has data members that shouldn’t be shallow copied, like pointers, resource ids (like file handles), etc.
A canonical implementation of a copy constructor
Implementing a copy constructor is straightforward and very similar to regular constructors. The only difference is that you have a single parameter which is a (const) reference to an object of that same type.
For the Product class, we can write the following:
class Product {
public:
explicit Product(int id, const std::string& name)
: id_{id}, name_{name}
{
std::cout << "Product(): " << id_ << ", " << name_ << '\n';
}
// copy constructor
Product(const Product& other)
: id_{other.id_}, name_{other.name_}
{ }
private:
int id_;
std::string name_;
};
As you can see, the copy constructor uses the member initialization list to copy the data from other. Please notice that there’s no need to use public getters, as we have access to all private data members. The compiler requires you to use a reference, so writing Product(Product other) won’t be treated as a copy constructor.
Here’s another example where logging is enabled:
#include <iostream>
#include <string>
class Product {
public:
explicit Product(int id, const std::string& name)
: id_{id}, name_{name}
{
std::cout << "Product(): " << id_ << ", " << name_ << '\n';
}
Product(const Product& other)
: id_{other.id_}, name_{other.name_}
{
std::cout << "Product(copy): " << id_ << ", " << name_ << '\n';
}
const std::string& Name() const { return name_; }
private:
int id_;
std::string name_;
};
int main() {
Product base { 42, "base product" }; // an initial object
std::cout << base.Name() << " created\n";
std::cout << "Product other { base };\n";
Product other { base };
std::cout << "Product another(base);\n";
Product another(base);
std::cout << "Product oneMore = base;\n";
Product oneMore = base;
std::cout << "Product arr[] = { base, other, oneMore };\n";
Product arr[] = { base, other, oneMore };
}
If you run the code, you should see the following output:
Product(): 42, base product
base product created
Product other { base };
Product(copy): 42, base product
Product another(base);
Product(copy): 42, base product
Product oneMore = base;
Product(copy): 42, base product
Product arr[] = { base, other, oneMore };
Product(copy): 42, base product
Product(copy): 42, base product
Product(copy): 42, base product
In the first line, we construct base product, and then use it to copy-construct all other instances.
A compiler-generated copy constructor
Content available in the full version of the book.
Move constructor
Move constructors take rvalue references of the same type.
ClassName(ClassName&&);
In short, rvalue references are temporary objects, usually appearing on the right-hand side of an expression and which value is about to expire.
For example:
std::string hello { "Hello"}; // lvalue, a regular object
std::string world { "World"}; // lvalue
std::string msg = hello + world;
Above, the expression hello + world creates a temporary object. It doesn’t have a name, and we cannot access it easily. Such temporary objects will end their lifetime immediately after the expression completes (unless it’s assigned to a const or rvalue reference4), so we can steal resources from them safely. It doesn’t make sense in the case of built-in types like integers or floats, as we need to copy values anyway. But in the case of strings or memory buffers, we can avoid data copy and just reassign the pointers.
Move constructors are a way to support the case with initialization from temporary objects. In many cases, they are an optimization over regular copy constructor calls. Additionally, they can also be used to pass “ownership” of the resource, for example, with smart pointers.
You can mark a regular object as expiring with the std::move function when you have a regular object with a name. This tells the compiler that the object’s value is no longer needed, so it’s safe to “steal” resources from it.
Have a look at this example:
#include <iostream>
#include <string>
class Product {
public:
explicit Product(int id, const std::string& name)
: id_{id}, name_{name}
{
std::cout << "Product(): " << id_ << ", " << name_ << '\n';
}
Product(Product&& other)
: id_{other.id_}, name_{std::move(other.name_)}
{
std::cout << "Product(move): " << id_ << ", " << name_ << '\n';
}
const std::string& name() const { return name_; }
private:
int id_;
std::string name_;
};
int main() {
Product tvSet {100, "tv set"};
std::cout << tvSet.name() << " created...\n";
Product setV2 { std::move(tvSet) };
std::cout << setV2.name() << " created...\n";
std::cout << "old value: " << tvSet.name() << '\n';
}
When you run the code, you can see the following output:
Product(): 100, tv set
tv set created...
Product(move): 100, tv set
tv set created...
old value:
As you can see, we create the first object, and then mark it as expiring. This gives a chance for the compiler to call the move constructor.
Product(Product&& other)
: id_(other.id_), name_(std::move(other.name_))
The above implementation is similar, but we need to pay attention to details. Since id_ is just an integer, all we can do is copy the value. We cannot perform any optimizations here. As for the name_ member, we can initialize it with std::move(other.name_). We encounter the first problem, other.name_ is a name, so not a temporary (a temporary has no name); we can not move (take, steal) its contents. That is why we tell the compiler to interpret it as temporary by using the expression std::move(other.name_). This will invoke the move constructor for std::string, and, potentially, “steal” the buffer from other.name_.
The move constructor must ensure that the other object is left in an unspecified but valid state. In our case, we can see it in the last line of the output. The line old value: ends with nothing, so the string was simply cleared.
noexcept and move constructors
Content available in the full version of the book.
A compiler-generated move constructor
Content available in the full version of the book.
Distinguishing from assignment
Content available in the full version of the book.
Adding logging to constructors
As an exercise, let’s add logging to our DataPacket class and see when each constructor is called:
DataPacket class. Run @Compiler Explorer 1 class DataPacket {
2 std::string data_;
3 size_t checkSum_;
4 size_t serverId_;
5
6 public:
7 DataPacket()
8 : data_{}
9 , checkSum_{0}
10 , serverId_{0}
11 { }
12
13 explicit DataPacket(const std::string& data, size_t serverId)
14 : data_{data}
15 , checkSum_{calcCheckSum(data)}
16 , serverId_{serverId}
17 {
18 std::cout << "Ctor for \"" << data_ << "\"\n";
19 }
20
21 DataPacket(const DataPacket& other)
22 : data_{other.data_}
23 , checkSum_{other.checkSum_}
24 , serverId_{other.serverId_}
25 {
26 std::cout << "Copy ctor for \"" << data_ << "\"\n";
27 }
28
29 DataPacket(DataPacket&& other)
30 : data_{std::move(other.data_)} // move string member...
31 , checkSum_{other.checkSum_} // no need to move built-in types...
32 , serverId_{other.serverId_}
33 {
34 other.checkSum_ = 0; // leave this in a proper state
35 std::cout << "Move ctor for \"" << data_ << "\"\n";
36 }
37
38 DataPacket& operator=(const DataPacket& other) {
39 if (this != &other) {
40 data_ = other.data_;
41 checkSum_ = other.checkSum_;
42 serverId_ = other.serverId_;
43 std::cout << "Assignment for \"" << data_ << "\"\n";
44 }
45 return *this;
46 }
47
48 DataPacket& operator=(DataPacket&& other) {
49 if (this != &other) {
50 data_ = std::move(other.data_);
51 checkSum_ = other.checkSum_;
52 other.checkSum_ = 0; // leave this in a proper state
53 serverId_ = other.serverId_;
54 std::cout << "Move Assignment for \"" << data_ << "\"\n";
55 }
56 return *this;
57 }
58
59 // getters/setters
60 };
And here’s the main() function:
DataPacket class, the main function. Run @Compiler Explorer 1 int main() {
2 DataPacket firstMsg {"first msg", 101 };
3 DataPacket copyMsg { firstMsg };
4
5 DataPacket secondMsg { "second msg", 202 };
6 copyMsg = secondMsg;
7
8 DataPacket movedMsg { std::move(secondMsg)};
9 // now we stole the data, so it should be empty...
10 std::cout << "secondMsg's data after move ctor): \""
11 << secondMsg.getData() << "\", sum: "
12 << secondMsg.getCheckSum() << '\n';
13
14 movedMsg = std::move(firstMsg);
15
16 // now we stole the name, so it should be empty...
17 std::cout << "firstMsg's data after move ctor): \""
18 << firstMsg.getData() << "\", sum: "
19 << firstMsg.getCheckSum() << '\n';
20 }
When you run the example, you should see the following output:
Ctor for "first msg"
Copy ctor for "first msg"
Ctor for "second msg"
Assignment for "second msg"
Move ctor for "second msg"
secondMsg's data after move ctor): "", sum: 0
Move Assignment for "first msg"
firstMsg's data after move ctor): "", sum: 0
The example creates several DataPacket objects, and with each creation, you can see that the compiler invokes the appropriate constructor or an assignment operator. For instance, in line 3, we need a copy constructor call. On the other hand, line 5 shows an assignment (copyMsg already exists). In the last section of main(), lines 8 and 14, there are calls to std::move(), which marks secondMsg and firstMsg as an rvalue reference, from which the contents could be moved. This means that the object is unimportant later, and we can “steal” from it. In this case, the compiler will call a move constructor or move assignment operator.
Trivial classes and user-declared/user-provided default constructors
Content available in the full version of the book.