Constructors and Destructors
| Rule 49. Declare constructors with one parameter as explicit. |
Constructors which take exactly one argument form a special case within C++, as they also provide an implicit type conversion from the arguments type to the class. Consider the following example as a clarification
class Array {
public:
Array();
Array(unsigned int size);
};
...
Array a(); // creates an array
Array b(10); // creates array of size 10
a = (Tiny)10; // creates array of size 10
|
Of course the last line looks very strange and should not be allowed in order to avoid bugs and make your code more readable. The problem here is that the second constructor also works as a conversion operator from unsigned int's to objects of type Array - probably not what you intended. Luckily C++ does not only offer this implicit conversion, it also offers the keyword explicit in order to prevent this. Thus the following declaration solves the problem:
class Array {
public:
Array();
explicit Array(unsigned int size);
};
|
Note that this problem only arises with constructors with exactly one argument, as a constructor with two or more parameters obviously cannot be used for conversions.
| Exception to 49. For transparent type conversion, omit the explicit keyword in front of the constructor. |
In most cases an implicit conversion is not desired, but there are also cases where it absolutely makes sense to enable this feature of C++, for example with simple classes representing numbers. Consider the following example.
class Complex {
public:
Complex();
Complex(float real);
Complex(float real, float imag);
};
|
In this case it does make much sense to offer a conversion from real numbers to complex numbers, so the explicit keyword is not needed.
| Recommendation 50. Constructors and destructors must not be inline. |
The construction and destruction of most objects is an expensive operation, it does not make sense to make constructors or destructors inline. This is especially true with derived classes or classes containing virtual methods. In the first case the constructor of the base class will also be called, which in turn enlarges the creation process and the second case the compiler will also store some information inside the object about its correct class in order to resolve virtual method invocations.
This is only a recommendation, because for many small and simple classes containing only a few simple data types an inline constructor can make sense in order to improve the speed of execution. On the other hand, if a class is instantiated using ''new'', you already have to pay a considerable amount of computation time in order to allocate the memory.
| Rule 51. A class which uses ''new'' to allocate instances managed by the class, must define a copy constructor. |
If you do not provide a copy constructor for a class, the compiler will create a default copy constructor which works as follows: For all member variables which are objects and do have a copy constructor, that constructor is used to duplicate the embedded object. For all other objects and simple data types a simple memcpy is performed. Of course this memcpy does not work very well together with pointers, because after this copy operation two objects have a member variable pointing to the same memory block. Even if this is desired in order to reduce the memory footprint, problems will arise when the destructor of the object is called, that probably will delete this memory block, regardless of any other object pointing to this block.
Consider the following example
class Block {
private:
void* m_Memory;
unsigned int m_Size;
public:
explicit Block(unsigned int size);
Block(const Block& );
~Block();
};
Block::Block(unsigned int size) {
m_Size = size;
m_Memory = new char[size];
}
Block::Block(const Block& block) {
m_Size = block.m_Size;
m_Memory = new char[m_Size];
memcpy(m_Memory, block.m_Memory, m_Size);
}
Block::~Block() {
delete[] (byte*)m_Memory;
}
|
Imagine what would happen when the following code is executed and we did not have a copy constructor:
Block b1(100); Block b2 = b1; |
Then both blocks b1 and b2 would share the same memory block, but if one of them was destroyed, the shared memory region would have been deleted too, although the second block object also refers to the same memory.
Of course the same problem arises with the assignment operator, see the next subsection for details.
| Recommendation 52. Do not write a copy constructor if shallow copies are sufficient. |
In such a case you should let the compiler do the work for you. Probably the compiler will have more chanced to produce faster code due to less constraints. You also do not need to write an explicit copy constructor if all embedded objects do have an (explicit or implicit) copy constructor satisfying your needs. The compiler will call these automatically.
| Rule 53. The destructor of a base class with virtual methods is to be declared as virtual. |
When you design a class with virtual methods, you expect that a class is accessed through the pointer to a base class. The same will can be true when an object is destroyed via delete. See the following example:
class BaseClass {
public:
virtual void g();
};
class DerivedClass : public BaseClass {
private:
char* m_Data;
public:
DerivedClass();
~DerivedClass();
void g();
};
DerivedClass::DerivedClass() {
m_Data = new char[100];
}
DerivedClass::~DerivedClass() {
delete[] m_Data;
}
|
Now let us consider the following usage of these classes:
// Construct new object BaseClass* base = new DerivedClass(); // Call virtual method via base pointer base->g(); // OK, calls DerivedClass::g() // Destroy object via base pointer delete base; // Bad, calls BaseClass::~BaseClass() |
In the example above, the destructor of DerivedClass will not be called and thus the memory allocated will not be freed again. This problem can easily be solved by the usage of a virtual destructor, as follows:
class BaseClass {
public:
virtual ~BaseClass();
virtual void g();
};
class DerivedClass : public BaseClass {
private:
char* m_Data;
public:
DerivedClass();
~DerivedClass();
void g();
};
|
Now the virtual keyword in front of the destructor of BaseClass ensures that always the correct destructor will be called, and in the last example the memory will be freed again by DerivedClass.
Kaya Memisoglu 2005-01-06
