In the previous article, we introduced why we should avoid writing copy constructors and value assignment operators. Today we will discuss why we should avoid writing code in the destructor. That is, leave the Destructor empty.
For example:
virtual ~MyClass(){}
We use the term null destructor to represent the Destructor without code in curly brackets.
You may need to write the Destructor for the following reasons:
- In the base class, you may need to declare a virtual destructor, so that you can use a pointer to the base class to point to an instance of a derived class.
- In a derived class, you do not need to declare the Destructor as a virtual function, but you can also do so to enhance readability.
- You may need to declare the Destructor without throwing any exceptions.
We will discuss the last case in detail. In C ++, it is considered a bad idea to throw an exception from the destructor. This is because destructor are often called when an exception has been thrown. if an exception is thrown again in this process, the program is terminated (or crashed ), this is likely to violate the programmer's original intention. Therefore, in some classes, the Destructor is declared as follows:
virtual ~ScppAssertFailedException() throw (){}
This means that we will not throw an exception from this destructor. Therefore, we can see that you sometimes need to write destructor. Now we can discuss why the Destructor should be empty. When do we need to show substantial code in the Destructor? Only when some resources are obtained in the Destructor or other methods of the class, and the class objects should be released when they are destroyed, such:
class PersonDescription{public:PersonDescription(const char* first_name, const char* last_name): first_name_(NULL), last_name_(NULL){if(first_name != NULL)first_name_ = new string(first_name);if(last_name != NULL)last_name_ = new string(last_name);}~PersonDescription(){delete first_name_;delete last_name_;}private:PersonDescription(const PersonDescription&);PersonDescription& operator = (const PersonDescription&);string* first_name_;string* last_name_;};
The design of this class violates the principles discussed in the previous articles. First, we can see that each time we add a new element that expresses the personal description, we need to add the corresponding cleaning code to the destructor, this violates the principle of "do not force the program to remember something. The following is the improved design code:
class PersonDescription{public:PersonDescription(const char* first_name, const char* last_name): first_name_(NULL), last_name_(NULL){if(first_name != NULL)first_name_ = new string(first_name);if(last_name != NULL)last_name_ = new string(last_name);}private:PersonDescription(const PersonDescription&);PersonDescription& operator = (const PersonDescription&);string* first_name_;string* last_name_;};
In this example, we do not need to compile the Destructor at all, because the compiler will automatically generate a destructor for us to complete these tasks, while reducing the workload, it also reduces the possibility of vulnerable code. However, this is not the main reason for choosing the second design. In the first example, there is a more serious potential hazard.
Suppose we decided to add a security check to check whether the caller provided the first and last names:
class PersonDescription{public:PersonDescription(const char* first_name, const char* last_name): first_name_(NULL), last_name_(NULL){<span style="color:#ff0000;">SCPP_ASSERT(first_name != NULL ,"First name must be provided");first_name_ = new string(first_name); SCPP_ASSERT(last_name != NULL ,"Last name must be provided");last_name_ = new string(last_name);</span>}~PersonDescription(){delete first_name_;delete last_name_;}private:PersonDescription(const PersonDescription&);PersonDescription& operator = (const PersonDescription&);string* first_name_;string* last_name_;};
As we discussed earlier, errors in the program may terminate the program, but an exception may also be thrown. Now we are in the trouble: throwing an exception from the constructor is a bad idea. Why? If we try to create an object on the stack and the constructor completes its tasks normally (without throwing an exception), after this object leaves the scope, its destructor will be called. However, if the constructor does not complete its task, but throws an exception, the Destructor will not be called.
Therefore, in the previous example, if we assume that the first name is provided but the last name is not provided, the strings with the first name will be allocated with memory, but will never be deleted, resulting in Memory leakage. However, the situation is not irrecoverable. Further, if we have an object that contains other objects, an important question is: which destructor will be called? Which destructor will not be called?
The following is a small test:
class A {public:A(){cout<<"Creating A"<<endl;}~A(){cout<<"Destroying A"<<endl;}};class B {public:B(){cout<<"Creating B"<<endl;}~B(){cout<<"Destroying B"<<endl;}};class C : public A{public:C(){cout<<"Creating C"<<endl;Throw "Don't like C";}~C(){cout<<"Destroying C"<<endl;}private:B b_;};
Note that class C contains class B by merging (that is, Class C has a data member of type B. It also inherits objects of the type (that is, there is an object of the type inside the object of the C type ). What happens if the C constructor throws an exception? See the following code:
int main(){cout<<"Testing throwing from constructor."<<endl;try{C c;}catch(...){cout<<"Caught an exception."<<endl;}return 0;}
The following output is generated after running:
Testing throwing from constuctor.Creating ACreating BCreating CDestroying BDestroying ACaught an exception.
Note that only the destructor of C are not executed, and the destructor of A and B are called. Therefore, the answer to the above question is simple and logical: for objects that allow the constructor to end normally, the Destructor will be called, even if these objects are part of a larger object, the latter constructor does not end normally. Therefore, let's use a smart pointer to rewrite the above sample code and introduce the security check:
class PersonDescription{public:PersonDescription(const char* first_name, const char* last_name): first_name_(NULL), last_name_(NULL){SCPP_ASSERT(first_name != NULL ,"First name must be provided");first_name_ = new string(first_name); SCPP_ASSERT(last_name != NULL ,"Last name must be provided");last_name_ = new string(last_name);}~PersonDescription(){delete first_name_;delete last_name_;}private:PersonDescription(const PersonDescription&);PersonDescription& operator = (const PersonDescription&);<span style="color:#ff0000;">scpp::ScopedPtr<string> first_name_;scpp::ScopedPtr<string> last_name_;</span>};
Even if 2nd security checks throw an exception, the Destructor pointing to first_name _ will still be called and cleaned up. Another benefit is that we don't need to worry about initializing these smart pointers to null, which is automatically completed. Therefore, throwing an exception from the constructor is a potential dangerous behavior: The corresponding destructor will not be called, so there may be problems, unless the Destructor is empty.
Summary:
When an exception is thrown from the constructor, The Destructor is kept empty during class design to avoid Memory leakage.
Avoid coding in destructor