Game Development traps brought about by a C ++ multi-Inheritance

Source: Internet
Author: User

Class Actor: public EventedSprite, public Path_Finder {...};

EventedSprite is an engine-provided genie class. It is an abstract class of visible objects in a game. It is used to process the animation and location of a game object and the relationship between the animation and map. Path_Finder is our path-finding class. I use inheritance relationships to reuse code. This is reasonable because the system is not complex and there are not many combinations of functions. I don't want to add any deeper subclass system to Path_Finder. It is just it -- it only handles pathfinding. (Later, I felt that it would be more reasonable to use object combinations without background computing)

Naturally, I began to associate the pathfinding class with background computing. On our game platform, the system provides the Task class (Task) to encapsulate threads, so that backend computing can have a better abstraction. So I designed these classes:
Class ConcurrentJob
{
Public:
Virtual void run () = 0;
};

Class Path_Finder: public ConcurrentJob
{
Public:
//... Other declarations

Virtual void run ()
{
// Perform pathfinding...
}

};

Class My_Task: public Task
{
Public:
Void setJob (void * job) {m_job = job ;}
Virtual void run ()
{
ConcurrentJob * job = static_cast <ConcurrentJob *> (m_job );
Job-> run ();
}
Private:
Void * m_job;
};

ConcurrentJob is a concurrent job class for games. It is a base class for background computing. Path_Finder is our path-finding class. It inherits from ConcurrentJob and implements the run method. This method is used for path-finding and called in the background. The My_Task class inherits from the system class Task and is used with ConcurrentJob. It sets a computing job and converts it into a ConcurrentJob in run for calculation. As a subclass of a Task, My_Task is handed over to the background running queue of the system.

You may ask why the My_Task: m_job is void * instead of directly writing it as ConcurrentJob *. Does this eliminate the need for casting in My_Task: run? Yes, you are right. But there are two things I need to do:

1) if you write this statement, it will not lead to errors caused by the use of multi-inheritance (MI). This is the direct source of errors.
2) In fact, in our system, the My_Task and Task classes are not written in C ++. I use two languages for cross programming, the language cannot recognize classes written in C ++ during Declaration, so it can only use void *. However, the implementation of My_Task: run should not be written in the declaration here, because it cannot be identified by the language. Now (all written in C ++) is to facilitate reading.

As a result, I wrote the startup code similar to the following during pathfinding computing:

My_Task * myTask = new My_Task;
Actor * actor = new Actor;
MyTask-> setJob (actor );
System_Task_Manager_Or_Something-> addToOperationQueue (myTask );

I created a My_Task instance, and added an Actor as a Path_Finder, that is, a concurrent job to the task, and then handed it to the system for running in the background. This seems okay until I run the program...

Program crashed! From non-main thread! The breakpoint is in My_Task: run. After analyzing the tension of the timer, I got the conclusion:

ConcurrentJob * job = static_cast <ConcurrentJob *> (m_job );

The casting fails and an invalid job pointer is obtained, which is used later.

Why does casting fail? According to the inheritance system, an Actor is a Path_Finder, and a Path_Finder is not a ConcurrentJob? Why does converting an Actor * into a ConcurrentJob * fail?


Seek Truth

After careful debugging and research on the code, the culprit was finally caught by the implicit Pointer Adjustment brought about by the C ++ multi-inheritance (MI) mechanism! This type of conversion is secretly completed by the compiler. The conversion itself is transparent to program developers and will not affect the program in general. However, our program has a special nature, leading to this serious error. Next, press the problem above into the stack, and let me tell you the cause and effect of the problem. This requires some basic knowledge of the C ++ object model, but please rest assured that I will include all the required knowledge.


Single Inheritance (SI) Object Model

Consider the following class:

Class
{
Public:
Int m_a;
Int m_ B;
};

A;

 

The layout of class object a in the memory is as follows:

The two member data are arranged in order. The address of class object a is the first address of the memory space. Now we add a class:

Class B: public
{
Public:
Int m_c;
};

B B;
A * a = & B; // upcast
B * b2 = static_cast <B *> (a); // downcast

The layout of class object B in the memory is as follows:

First, the subobject memory layout of Class A, and then the data member of Class B. The C ++ standard ensures that "the base class subobject In the derived class remains unchanged ". Therefore, whatever the layout of class A, it will be completely stored in the memory model of class B, which mainly considers the compatibility with C. Note the following ):

1) The padding bytes generated by class A due to memory alignment must also appear in class A subobject of class B, which ensures that the basic class subobject is strictly unchanged.
2) For Class systems with virtual functions, vptr can be placed in two ways according to different compilers: header and tail. For the compiler placed in the header, if A virtual destructor is added to Class B, so that Class A does not have the virtual mechanism and class B has the virtual mechanism, the header of class object B is vptr rather than class A subobject. However, this does not affect the consistency of pointers.
3) If B is virtual and inherits from A, there are other variables. In the words of Stanley B. Lippman, "Once a virtual base class is applied to any rule, nothing will happen ". This topic is not discussed here.

& B, a, and b2 all point to the first address of B. Therefore, in the SI model, the object memory uses an overlapping model. The pointer of the base class and any derived class points to the first address of the object, therefore, the address values of these pointers are the same-all base classes subobject share the same first address.

That is to say, in an inheritance system, no matter how you perform downcast or upcast on a pointer pointing to an object, the pointer address value is the same. Add a level of system as follows:

Class C: public B
{
Public:
Int m_d;
};

 

The class pointers of A, B, and C in the same system are casting each other and get the same address.


Multiple Inheritance (MI) Object Model

The MI mechanism is one of the features of the C ++ language and one of the culprit in complexity! Here, the compiler has done some things with us, which is also the main reason for C ++ criticism.

Consider the following program:

Class
{
Public:
Int m_a;
};

Class B
{
Public:
Int m_ B;
};

Class C
{
Public:
Int m_c;
};

Class D: public A, public B, public C
{
Public:
Int m_d;
};

A *;
B * B;
C * c;
D;
A = & d;
B = & d;
C = & d;

Class relationship:

 

This is the simplest MI system. D inherits from three base classes. Let's take a look at its memory model:

 

We can see that, unlike SI, MI uses a non-overlapping model-each base class subobject has its own first address. Here, A, B, and C subobject each occupy their own first addresses. The only exception is D object, which is the owner of the model, its first address is the same as class A subobject. Therefore, we say:

Assert (a ==& d );
Assert (B! = & D );
Assert (c! = & D );

"Ah! Wait !", I heard you interrupt me, "we have stated in the above program

B = & d;
C = & d;

Here why do you write this:

Assert (B! = & D );
Assert (c! = & D );

Are you sure you want to assert that it won't crash ?". If you ask me this question, I'm glad that you are following me. The data I obtained through the experiment is as follows:

 

This is the key to the problem-the compiler has done one thing behind our back: this Pointer Adjustment! In the MI world, this pointer is adjusted very frequently, and this adjustment mainly occurs in the derived class object and the "second and subsequent base class Object" (like a spell). In the above example, "the second and subsequent base classes" are Class B and C. This conversion is

B = & d; // upcast
C = & d; // upcast

This pointer is adjusted by compiler at this time. B and c point to the correct subobject addresses respectively. Similarly, when we convert B and c to d pointers, this pointers will also be adjusted.

D * d2 = static_cast <D *> (B); // downcast
D * d3 = static_cast <D *> (c); // downcast

The result is:

Assert (d2 = & d );
Assert (d3 = & d );

The pointer is adjusted back. This will not happen in the SI world (overlapping models ).

Why adjust the this pointer? The reason for this pointer adjustment is that MI uses a non-overlapping memory model, which is used to ensure the integrity and independence of each base-class system, ensure that the virtual mechanism can run smoothly between different MI systems (this is done through the respective vptr of each subobject ). About MI and Its this Pointer Adjustment, we can say that something is enough to be written into a book (this article is just the tip of the iceberg). Of course it won't work here! Any theoretical questions about MI can be found in The book Inside The C ++ Object Model.

However, if you understand all the theories discussed above, you will be able to understand the following and general MI problems.

Problem Analysis

After mastering the basic knowledge of SI and MI, we can now bring up the previous issue stack! We leave the lab to analyze the problems in our real life. The Actor inheritance system is as follows:

 

The old method is to analyze its memory model:

 

This system is a mixture of SI and MI. Actor can be regarded as the MI class of the left and right systems. Super hierarchy and EventedSprite SI as the first base class. The SI of ConcurrentJob and Path_Finder is regarded as the second base class. Therefore,

Class Actor: public EventedSprite, public Path_Finder {...}

Related:

Actor actor;
EventedSprite * spr = & actor; // 1
Path_Finder * path = & actor; // 2

Assert (spr ==& actor );
Assert (path! = & Actor );

Because this pointer is adjusted in step 2-this is clear. Well, let's look at our problematic program:

My_Task * myTask = new My_Task;
Actor * actor = new Actor;
MyTask-> setJob (actor );
System_Task_Manager_Or_Something-> addToOperationQueue (myTask );

We handed the actor to the My_Task: setJob method. The parameter of this method is type void *. It can accept any pointer type. This is no problem. We only need to store this address, use it when necessary. Let's look at My_Task: run:

Virtual void run ()
{
ConcurrentJob * job = static_cast <ConcurrentJob *> (m_job );
Job-> run ();
}

M_job is the stored Actor * -- this address is correct. However, the m_job type is void * -- no type information! We should

ConcurrentJob * job = static_cast <ConcurrentJob *> (m_job );

What are your expectations? We expect compiler to adjust this pointer for us! Because ConcurrentJob is the second base class system, do you still remember the "second and subsequent base classes" spell? A void * pointer. We use the casting operator During the compilation period.

Static_cast

There will be no changes in the address! The Actor * address is directly assigned to the ConcurrentJob *. This pointer is not adjusted! This pointer does not point to the correct subobject! Cause a serious error!

A quick solution is to first convert m_job into Actor * and then convert it. However, in any case, as long as compiler can provide enough type of information, it can do the right thing-but the premise is that you must first do the right thing.

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.