12.2. Copying objects¶
C++ is one of the few languages that provides precise control over how memory is managed. In C++11, every class is created with 5 special functions:
destructor
copy constructor
move constructor
copy assignment operator
move assignment operator
In many classes, such as most of those written so far, the compiler generated default versions work fine. As we will see, sometimes you have to write them yourself.
Programmers have choices on how (or if) objects are copied and moved. Whenever an object is passed by value to a function, or returned by value from a function, a copy is implicitly performed:
std::vector<int> scores;
auto avg = average (scores); // a copy of scores is passed to average
Copy operations also occur in range-for loops:
for (const int value: scores)
Each member of scores is copied into value on each iteration.
Explicit copying can also be performed. Whenever you have an existing object and use it to initialize a new or existing object, the copy constructor is called:
std::vector<std::string> words;
std::vector<std::string> w2 = words; // copy words into w2
12.2.1. Copy constructors¶
Both explicit and implicit copies are controlled by a special constructor called the copy constructor. Like other constructors, the copy constructor is a member function with the same name as the class name. The signature must be able to evaluate to this:
class_name ( const class_name & );
A copy constructor may take other parameters, but that is uncommon. If there are other parameters, they must all have defaults values defined. In general, the default copy constructor generated by the compiler will suffice. If the default creation is inhibited for any reason, it is acceptable to explicitly declare the default constructor:
class_name ( const class_name & ) = default;
However the default copy constructor is created, the behavior is the same: each class member is copied, in initialization order.
Copy constructor
A simple class with a default and a copy constructor.
struct A
{
int n;
double d;
// user defined default constructor
A(int n = 0, double d = 1)
: n{n}
, d{d}
{ }
// user defined copy constructor
A(const A& other)
: n{other.n}
, d{other.d}
{ }
};
In this case, the user defined copy constructor does what the default constructors would do. When that is the case, it's best not to redo the work of the compiler.
Run It
1#include <iostream>
2
3struct A
4{
5 int n;
6 double d;
7
8 // user defined default constructor
9 A(int n = 0, double d = 1)
10 : n{n}
11 , d{d}
12 {
13 std::cout << "default A\n";
14 }
15
16 // user defined copy constructor
17 A(const A& other)
18 : n{other.n}
19 , d{other.d}
20 {
21 std::cout << "copy into A\n";
22 }
23};
24
25int main() {
26 A a;
27 A b = a;
28 return b.n;
29}
When objects manage their own resources,
simple member-wise assignment cannot be used.
Consider a small mesa::string class.
It owns a dynamically allocated character array, but it does not define
its own copy constructor.
That means the compiler-generated copy constructor copies each data member
directly, including the pointer.
1#include <algorithm>
2#include <cctype>
3#include <cstddef>
4#include <iostream>
5#include <locale>
6#include <string_view>
7
8namespace mesa {
9 class string {
10 public:
11 string() = default;
12
13 explicit string(std::string_view source)
14 : size_{source.size() + 1},
15 data_{new char[size_]}
16 {
17 std::copy(source.begin(), source.end(), data_);
18 data_[source.size()] = '\0';
19 }
20
21 void upper_case() {
22 for (std::size_t i = 0; i + 1 < size_; ++i) {
23 data_[i] = std::toupper(data_[i], std::locale());
24 }
25 }
26
27 ~string() {
28 // Uncomment this line after running the program once.
29 // A correct string class must release its memory.
30 // What happens if we cleanup the string memory?
31 // delete[] data_;
32 }
33
34 const char* c_str() const {
35 return data_ == nullptr ? "" : data_;
36 }
37
38 private:
39 std::size_t size_ = 0;
40 char* data_ = nullptr;
41 };
42} // namespace mesa
43
44int main() {
45 mesa::string hello{"Hello, world!"};
46 mesa::string copy = hello;
47
48 copy.upper_case();
49
50 std::cout << "hello: " << hello.c_str() << '\n';
51 std::cout << "copy: " << copy.c_str() << '\n';
52}
Even though we copied hello,
changing the case of copy also changed the original.
The compiler-generated copy constructor copied the pointer value,
not the character array pointed to by the pointer.
After the copy, both objects point to the same memory.
When we copy a value, we expect a cloned object: an object that has the same value, but is separate and distinct. We do not want changes in one object to affect the other.
The destructor body is commented out so the program can run by default.
If you uncomment delete[] data_; and run the program again,
the shallow copy has an even more serious consequence:
both objects will try to release the same memory.
Because there are two pointers to the same data on the free store, when either is deleted, the free-store memory is recovered. Consider this sequence:
mesa::string hello{"Hello, world!"};
// create a new scope
{
mesa::string copy = hello;
} // local variable copy destroyed
std::cout << hello.c_str() << '\n';
What does the last line print if the destructor deletes data_?
Show
There is no way to know for sure.
A modern compiler should detect the double delete and fail to compile. But that behavior is not perfect.
When copy goes out of scope and its destructor is called,
it deletes the memory copy::data points to,
but this is the same array hello is using.
When hello.c_str() is called, undefined behavior is the result.
Fixing these problems requires writing a custom copy constructor.
Each class member needs to be copied correctly.
The member size_ can simply be copied.
The pointer member needs special treatment:
Initialize a new memory block large enough to hold the copy
Copy each element of the source array into the destination.
In contrast to a shallow copy, this copy is a deep copy. It does not copy the pointer value. It creates an entirely new pointer and copies all of the data pointed to by the source pointer to the destination.
Try This!
Add a copy constructor to mesa::string.
The copy should preserve the value of the source string,
but the copy and the original should not share the same character array.
After the copy constructor works, uncomment the destructor body and confirm that the program still runs.
12.2.2. Copy assignment¶
The copy assignment operator is similar to the copy constructor. The key difference to remember is that a copy constructor is only called when the left hand side object does not yet exist: it is in the process of being constructed.
X& X::operator=(const X& other)
{
// copy other content into this
return *this;
}
Copy assignment
Copy assignment is called when both already exist and you want to copy the right hand side object into the left hand side object.
struct A
{
int n;
double d;
// user defined default constructor
A(int n = 0, double d = 1)
: n{n}
, d{d}
{ }
// user defined copy constructor
A(const A& other)
: n{other.n}
, d{other.d}
{ }
A& operator=(const A& other)
{
if (this == &other) { return * this; }
n = other.n;
d = other.d;
return * this;
}
};
Run It
1#include <iostream>
2
3struct A
4{
5 int n;
6 double d;
7
8 // user defined default constructor
9 A(int n = 0, double d = 1)
10 : n{n}
11 , d{d}
12 {
13 std::cout << "default A\n";
14 }
15
16 // user defined copy constructor
17 A(const A& other)
18 : n{other.n}
19 , d{other.d}
20 {
21 std::cout << "copy into A\n";
22 }
23 A& operator=(const A& other)
24 {
25 if (this == &other) { return * this; }
26 std::cout << "copy assign A\n";
27 n = other.n;
28 d = other.d;
29 return * this;
30 }
31
32};
33
34int main() {
35 A a;
36 A b = a;
37 a = b;
38 return b.n;
39}
Try This!
Write a copy assignment function for the mesa::string class.
More to Explore
From cppreference.com: