Following is my note of reading effective c++ (this is an on-going post)
Copy all parts of an object
In a mature object-oriented C++ system, there are only two ways of copying objects: copy constructors
and assignment operators
referred to as copy
functions. Copy functions are compiler-generated functions by default, and the default copy function does copy the object in its entirety. Still, sometimes we choose to overload
the copy function. And that’s where the problem arises!
A correct implementation of the copy
function would look like this:
1
2
3
4
5
6
7
8
9
class Customer{
string name;
public:
Customer(const Customer& rhs): name(rhs.name){}
Customer& operator=(const Customer& rhs){
name = rhs.name; // copy rhs's data
return *this;
}
};
Perfect, right? But then, one day, we add a new data member and forget to update the copy function:
1
2
3
4
5
6
7
8
9
10
class Customer{
string name;
Date lastTransaction;
public:
Customer(const Customer& rhs): name(rhs.name){}
Customer& operator=(const Customer& rhs){
name = rhs.name; // copy rhs's data
return *this;
}
};
The lastTransaction is ignored and the compiler does not give any warnings (even at the highest warning level). Another common scenario is when we inherit from a parent class:
1
2
3
4
5
6
7
8
9
10
11
class PriorityCustomer: public Customer {
int priority;
public:
PriorityCustomer(const PriorityCustomer& rhs)
: priority(rhs.priority){}
PriorityCustomer&
operator=(const PriorityCustomer& rhs){
priority = rhs.priority;
}
};
The above code looks fine, but we forgot to copy the part of the parent class:
1
2
3
4
5
6
7
8
9
10
11
12
class PriorityCustomer: public Customer {
int priority;
public:
PriorityCustomer(const PriorityCustomer& rhs)
: Customer(rhs), priority(rhs.priority){}
PriorityCustomer&
operator=(const PriorityCustomer& rhs){
Customer::operator=(rhs);
priority = rhs.priority;
}
};
In short, when we implement the copy function:
- make a complete copy of the current object’s data (local data).
- call all the corresponding copy functions in the parent class.
You may notice the repetition of the code, but don’t let the copy
constructor and the assignment
operator call each other. They have completely different semantics! C++ doesn’t even provide a syntax for the assignment
operator to call the copy constructor. Conversely, it would compile to have the copy
constructor call the assignment
operator. But since the precondition for the copy
constructor is an uninitialized
object, the precondition for the assignment
operator is an initialized
object. Such a call is not a good design and may cause logical confusion.
Use object to manage resources
Those who are familiar with smart pointers will certainly not find this strange:
1
2
3
4
5
6
{
// ...
Binder *pBin = binder_factory.createBinder();
// ...
delete pBin;
}
The C++ compiler does not provide an automated garbage collection mechanism, so it’s the programmer’s responsibility to release resources. We are always asked to use new and delete in pairs. The above code does work properly without leaking memory. However, the problem is that the createBinder()
function shifts the responsibility of releasing the resource over to the caller, but does not explicitly declare this. Hence, the caller is sometimes unaware of it. Even if the caller knows that the resource needs to be destroyed, it may not be released in time due to flow control statements or exceptions.
Fortunately, we can wrap the resource in an object and release it in a destructor. This eliminates the need for clients to maintain the resource’s memory. One such object is std::unique_ptr
, called a smart pointer. A typical usage scenario where resources are stored in heap space but only used locally is:
1
2
3
void f(){
std::unique_ptr<Binder> pBin(binder_factory.createBinder());
}
At the end of the f()
call pBin
exits the scope and the destructor is called, eventually causing the resource to be freed. It would be better to have createInvestment return a smart pointer directly. As you can see, the key to using objects to manage resources is to put them into a resource management object as soon as they are created, and to use the resource management object’s destructor to ensure that they are released.
The framework for implementing the resource management object is exactly the RAII
principle: acquisition is initialization
, using a resource to initialize a smart pointer. The resource is released in the pointer’s destructor.
With RAII
, instead of writing
1
2
3
4
5
6
7
std::mutex mtx;
void f(){
mtx.lock();
//...
mtx.unlock();
}
we should be writing
1
2
3
4
5
6
std::mutex mtx;
void f(){
std::lock_guard<std::mutex> guard(mtx);
//...
}