Book Image

Learning Boost C++ Libraries

By : Arindam Mukherjee
Book Image

Learning Boost C++ Libraries

By: Arindam Mukherjee

Overview of this book

Table of Contents (19 chapters)
Learning Boost C++ Libraries
About the Author
About the Reviewers

Copy semantics

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>
 3 class Foo {
 4 public:
 5   Foo() {}
 7   Foo(const Foo&) {
 8     std::cout << "Foo(const Foo&)\n";
 9   }
11   ~Foo() {
12     std::cout << "~Foo()\n";
13   }
15   Foo& operator=(const Foo&) {
16     std::cout << "operator=(const Foo&)\n";
17     return *this;
18   }
19 };
21 class Bar {
22 public:
23   Bar() {}
25 private:
26   Foo f;
27 };
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);
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&)

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.

The nothrow swap

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.


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.