An object keeps state information in its data members, which can themselves be of POD-types or class types. If you do not define a copy constructor for your class, then the compiler implicitly defines one for you. This implicitly-defined copy constructor copies each member in turn, invoking the copy constructor of members of class type and performing a bitwise copy of POD-type members. The same is true of the assignment operator. The compiler generates one if you do not define your own, and it performs member-wise assignment, invoking the assignment operators of member objects of class-type, and performing bitwise copies of POD-type members.
The following example illustrates this:
Listing A.2: Implicit destructor, copy constructor, and assignment operator
1 #include <iostream> 2 3 class Foo { 4 public: 5 Foo() {} 6 7 Foo(const Foo&) { 8 std::cout << "Foo(const Foo&)\n"; 9 } 10 11 ~Foo() { 12 std::cout << "~Foo()\n"; 13 } 14 15 Foo& operator=(const Foo&) { 16 std::cout << "operator=(const Foo&)\n"; 17 return *this; 18 } 19 }; 20 21 class Bar { 22 public: 23 Bar() {} 24 25 private: 26 Foo f; 27 }; 28 29 int main() { 30 std::cout << "Creating b1\n"; 31 Bar b1; 32 std::cout << "Creating b2 as a copy of b1\n"; 33 Bar b2(b1); 34 35 std::cout << "Assigning b1 to b2\n"; 36 b2 = b1; 37 }
Class Bar
contains an instance of class Foo
as a member (line 25). Class Foo
defines a destructor (line 11), a copy constructor (line 7), and an assignment operator, (line 15) each of which prints some message. Class Bar
does not define any of these special functions. We create an instance of Bar
called b1
(line 30) and a copy of b1
called b2
(line 33). We then assign b1
to b2
(line 36). Here is the output when the program is run:
Creating b1 Creating b2 as a copy of b1 Foo(const Foo&) Assigning b1 to b2 operator=(const Foo&) ~Foo() ~Foo()
Through the messages printed, we can trace the calls made to Foo
's special functions from Bar
's implicitly generated special functions.
This works adequately for all cases except when you encapsulate a pointer or non-class type handle to some resource in your class. The implicitly-defined copy constructor or assignment operator will copy the pointer or handle but not the underlying resources, generating an object which is a shallow copy of another. This is rarely what is needed and this is where a user-defined copy constructor and assignment operator are needed to define the correct copy semantics. If such copy semantics do not make sense for the class, the copy constructor and assignment operator ought to be disabled. In addition, you would also need to manage resource lifetimes using RAII, and therefore define a destructor rather than relying on the compiler-generated one.
There is a well-known rule called the Rule of Three that regularizes this common idiom. It says that if you need to define your own destructor for a class, you should also define your own copy constructor and assignment operator or disable them. The String
class we defined in listing A.1 is such a candidate and we will add the remaining two of the three canonical methods shortly. As we noted, not all classes need to define these functions, only those that encapsulate resources. In fact, it is recommended that a class using these resources should be different from the class managing the lifetime of these resources. Thus, we should create a wrapper around each resource for managing that resource using specialized types like smart pointers (Chapter 3, Memory Management and Exception Safety), boost::ptr_container
(Chapter 5, Effective Data Structures beyond STL), std::vector
, and so on. The class using the resources should have the wrappers rather than the raw resources as members. This way, the class using the resource does not have to also bother about managing the resource life cycles, and the implicitly-defined destructor, copy constructor, and assignment operator would be adequate for its purposes. This has come to be called the
Rule of Zero.
Thanks to Rule of Zero, you should rarely need to bother about the Rule of Three. But when you do have to use the Rule of Three, there are a few nitty-gritties to take care of. Let us first understand how you would define a copy operation for the String
class in listing A.1:
Listing A.1a: Copy constructor
1 String::String(const String &str) : buffer_(0), len_(0)
2 {
3 buffer_ = dupstr(str.buffer_, len_);
4 }
The implementation of copy constructor is no different than that of the constructor in listing A.1. The assignment operator requires more care. Consider how String
objects are assigned to in the following example:
1 String band1("Deep Purple"); 2 String band2("Rainbow"); 3 band1 = band2;
On line 3, we assign band2
to band1
. As part of this, band1
's old state should be deallocated and then overwritten with a copy of band2
's internal state. The problem is that copying band2
's internal state might fail, and so band1
's old state should not be destroyed until band2
's state has been copied successfully. Here is a succinct way to achieve this:
Listing A.1b: Assignment operator
1 String& String::operator=(const String& rhs) 2 { 3 String tmp(rhs); // copy the rhs in a temp variable 4 swap(tmp); // swap tmp's state with this' state. 5 return *this; // tmp goes out of scope, releases this' 6 // old state 7 }
We create tmp
as a copy of rhs
(line 3) and if this copying fails, it should throw an exception and the assignment operation would fail. The internal state of the assignee, this
, should not change. The call to swap
(line 4) executes only if the copying succeeded (line 3). The call to swap
exchanges the internal states of this
and the tmp
object. As a result, this
now contains the copy of rhs
and tmp
contains the older state of this
. At the end of this function, tmp
goes out of scope and releases the old state of this
.
Tip
It is possible to optimize this implementation further by considering special cases. If the assignee (left-hand side) already has storage that is at least as large as needed to contain the contents of rhs
, then we can simply copy the contents of rhs
into the assignee, without the need for extra allocation and deallocation.
Here is the implementation of the swap
member function:
Listing A.1c: nothrow swap
1 void String::swap(String&rhs) noexcept 2 { 3 using std::swap; 3 swap(buffer_, rhs.buffer_); 4 swap(len_, rhs.len_); 5 }
Exchanging variables of primitive types (integers, pointers, and so on) should not cause any exceptions to be thrown, a fact we advertise using the C++11 keyword noexcept
. We could have written throw()
instead of noexcept
, but exception specifications are deprecated in C++11 and noexcept
is more efficient than a throw()
clause. This swap function, written entirely in terms of exchanging primitive data types, is guaranteed to succeed and would never leave the assignee in an inconsistent state.