C ++ tips: Avoid destructor calling virtual functions

Source: Internet
Author: User

If you have switched from another language such as C # or Java to C ++, you will feel that, it is intuitive to avoid calling virtual functions in class constructor or destructor. However, in C ++, violating this principle will cause unpredictable consequences and endless troubles.

Body

I want to start with repeating the topic of this article: Do not call virtual functions in class constructor or destructor, because such calls won't be as expected, even if it succeeds, in the end, it will make you frustrated. If you were a Java or C # programmer, pay close attention to the content in this section-this is one of the major differences between C ++ and other languages.

Suppose you have a class hierarchy that is modeled for stock trading, such as purchasing orders, selling orders, and so on. It is very important to establish an audit system for such transactions. In this way, an appropriate entry item is generated on the Audit Logon item whenever a transaction object is created. This seems to be a reasonable way to solve the problem:

  

Class Transaction {// The base class of all transactions

Public:

Transaction ();

Virtual void logTransaction () const = 0; // create a logon item dependent on the specific transaction type

...

};

Transaction: Transaction () // implement the constructor of the base class

{

...

LogTransaction (); // Finally, log on to the transaction

}

Class BuyTransaction: public Transaction {

// Derived class

Public:

Virtual void logTransaction () const; // how do I log on to this type of transaction?

...

};

Class SellTransaction: public Transaction {

// Derived class

Public:

Virtual void logTransaction () const; // how do I log on to this type of transaction?

...

};

Now, analyze what happened when executing the following code calls:

BuyTransaction B;

Obviously, a BuyTransaction class constructor is called. However, the Transaction class constructor is called first (the base class part of the derived class object is constructed before the derived class part ). The last line of the Transaction constructor calls the virtual function logTransaction, But the strange thing is happening here. The version of the called function logTransaction is the one in Transaction, not the one in BuyTransaction-even if the type of the generated object is BuyTransaction. During the construction of the base class, the virtual function call is never passed to the derived class. Instead, the behavior of the derived class Object seems to be a base type.

Non-standard, during the construction of the base class, the virtual function is not "constructed ".

The above seemingly intuitive behavior can be explained for one reason:

Because the base class constructor is executed before the derived class, the data members of the derived class are not initialized when the base class constructor is running. If the call to the virtual function is passed to the derived class during the construction of the base class, the object of the derived class can reference the local data member, but the data member is not initialized. This will lead to endless undefined behaviors and overnight code debugging. Calling some parts of an object that has not been initialized along the class hierarchy is inherently dangerous, so C ++ won't let you do this.
In fact, there are more basic requirements than this. During the construction of the base class Object of the derived class object, the type of this class is the base class type. Not only does the virtual function depend on the base class, but it also treats the object as the base class type using the corresponding part of the language of runtime information (for example, dynamic_cast (see Item 27) and typeid. In our example, when the Transaction constructor is running to initialize the base class part of the BuyTransaction object, the object is of the Transaction type.

This is done everywhere in C ++ programming. This makes sense: In the initialization of the base class object, the BuyTransaction part of the derived class object is not initialized, therefore, it is safest to treat these parts as nonexistent at the moment. Before the constructor of a derived class object starts execution, it will not become a derived class object.

The same logic exists in the object structure. Once the destructor of a derived class runs, the data members of the derived class of this object are assumed to be undefined values. Therefore, C ++ regards them as non-existent values. Once you enter the destructor of the base class, the object becomes a base class object, and all parts of C ++ (virtual functions, dynamic_cast operators, and so on) are processed in this way.

In the above sample code, the Transaction constructor directly calls a virtual function, which obviously breaks the principle emphasized in this article. This type of destructiveness is very noticeable, and some compilers give warnings (NOTE: Some compilers do not give warnings. For details, refer to the warning discussion in Item 53 ). Even if no warning is given, this problem is quite obvious during code runtime, because the logTransaction function is a pure virtual function in the Transaction class. Unless this function is defined (the possibility is not very high, but it does exist-see Item 34), the program will not be linked: the linker cannot find Transaction :: the required implementation code of logTransaction.

Calling virtual functions in class construction or destructor is not always so easy to discover. If the Transaction class has multiple constructors and each of them must execute some of the same tasks, only good software engineers can avoid code duplication, this can be achieved by placing the same initialization Code (including calling logTransaction) in a private and non-virtual initialization function, such as the following init:

  

Class Transaction {

Public:

Transaction ()

{Init () ;}// call a non-virtual function...

Virtual void logTransaction () const = 0;

...

Private:

Void init ()

{

...

LogTransaction (); // note that the virtual function is called here.

}

};

This code is conceptually the same as the previous version, but it is more risky, because in typical cases, the code is successfully compiled and linked. In this case, because logTransaction is a pure virtual function in the Transaction class, the vast majority of runtime systems will abort the program when the pure virtual function is called (typically by sending a message with the meaning of calling this function. However, if logTransaction is a "normal" virtual function "(that is, not purely virtual) and has its implementation in Transaction, this code segment will be called and the program will run smoothly for a period of time. This allows you to consider why the logTransaction version is called when a derived class object is created. The only way to avoid this problem is to ensure that no constructor or destructor calls a virtual function on an object being generated or destroyed, in addition, all the functions it calls must follow the same constraints.

However, whenever an object is generated in the Transaction class hierarchy, how can we ensure that the correct version of logTransaction is called? Obviously, calling virtual functions on objects from the Transaction constructor is incorrect.

There are several different ways to solve this problem. One way is to change the logTransaction function to a non-virtual function in Transaction, and then require the constructors of the derived subclass to pass the necessary login information to the Transaction constructor. So far, the above function can safely call the non-virtual function logTransaction. As follows:

  

Class Transaction {

Public:

Explicit Transaction (const std: string & logInfo );

Void logTransaction (const std: string & logInfo) const; // a non-virtual function

...

};

  

Transaction: Transaction (const std: string & logInfo)

{

...

LogTransaction (logInfo); // currently, a non-virtual function is called.

}

  

Class BuyTransaction: public Transaction {

Public:

BuyTransaction (parameters)

: Transaction (createLogString (parameters) {...} // transfers the logon information to the constructors of the base class.

...

Private:

Static std: string createLogString (parameters );

};

In other words, since the base class constructor cannot call the virtual function down the hierarchy of the class, you can compensate for this by passing necessary constructor information to the constructor of the base class along the hierarchy of the class in the derived class.

In this example, pay attention to the use of the Private Static function createLogString in BuyTransaction. Use the help function to create a value and pass it to the base class constructor, this method is more convenient and readable than the operations required to implement the base class in the member initialization list. Here we create this function as a static type, which is not dangerous for occasional reference to the uninitialized data members of the newly generated BuyTransaction object. This is important because the data members are still in an undefined state, this fact explains why calling a virtual function in the construction or destructor of the base class cannot be passed to the derived subclass first.


Conclusion

Do not call virtual functions during class construction or destructor, because such calls will never be passed down to the subclass along the class inheritance tree.

Related Article

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.