2006-02-13

Engine Update: Inheritance

So the plan is to rewrite parts of the engine with a new inheritance order. The existing implementation is as follows:

/*
* A class defining all of the machine specific variables and
* operations that can only be done at this level. Should contain
* little or no code that can be shared cross-platform. (eg. The
* DirectX Windows version of this file does anything that is
* specific to DirectX or Windows).
*/

class MachineClass
{
private:
MachineType m_machine_var; // Stores machine specific data.
SharedType m_shared_var; // Stores common data that
// machine needs access to.
// [I never liked having to do this.]


public:
MachineClass();
~MachineClass();

void Func1( int in_params )
{
// do some stuff specific to this platform.
...
}
};


/*
* This class implements anything that can be shared cross-platform.
*/

class CommonClass : public MachineClass
{
public:
CommonClass();
~CommonClass();

void Func1( int in_params )
{
MachineClass::Func1( in_params );

// do anything that can be shared cross-platform here or
// before the call to Func1, whichever is more appropriate.

...
}

void Func2()
{
// Do something that is shared cross-platform.
...
}
};


Anyone who has worked with me in the past will notice a similarity to the implementation by my former employer. I never loved this implementation, but I decided to stick with what I knew. After all I was used to it and liked the idea that without anything being virtual there wasn't any overhead associated with the function calls. However I still never quite liked the way this felt. It always seemed overly cautious in terms of sacrificing the benefits of C++ for the sake of a perceived boost in performance. Of course it was our first C++ engine, implemented by mostly C coders, and we never could all agree on implementation specifics anyway.

As I look at it now, I'm not sure that this approach is what I want, so I'm attempting to implement everything in a much more C++, Object Oriented manner. This will add a little overhead due to the virtual functions, but the benefits gained will far outway any decrease in performance. Plus, most of these are large classes with only a few function calls per frame. These aren't classes that are called repeatedly. So here's the proposed new implementation.

/*
* This class defines the interface and implements anything that
* can be shared cross-platform. This class will be shared by all
* platforms.
*/

class CommonClass
{
private:
SharedType m_shared_var; // Now it can be declared here,
// but used by the derived class.


public:
CommonClass();
virtual ~CommonClass();

virtual void Func1( int in_params )
{
// do anything that can be shared cross-platform here.
...
}

void Func2();

const SharedType& GetSharedData() const;
{
return m_shared_var;
}

void SetSharedData( const SharedType& shared_data )
{
m_shared_data = shared_data;
}
};

/*
* A class defining all of the machine specific variables and
* operations that can only be done at this level. Should contain
* little or no code that can be shared cross-platform. (eg. The
* DirectX Windows version of this file does anything that is
* specific to DirectX or Windows).
*/

class MachineClass : public CommonClass
{
private:
MachineType m_machine_var; // Stores machine specific data.

public:
MachineClass();
virtual ~MachineClass();

virtual void Func1( int in_params )
{
// If neccesary call down to the base class to run
// the common code.

CommonClass::Func1( in_params );

// do some stuff specific to this platform.
...
}
};


So basically it boils down to switching the order of the inheritance chain. With this model the Common class specifies the interface that is to be shared by all platforms, and anything that needs to be done that is platform specific is done by defining the function in the derived Machine class. Much nicer.


Pros & Cons

PRO
--> This allows me to define as many different machine implementations as I want to. The Common class will behave as a Factory and create classes based on the type I desire. For my purposes this will be based on the graphics API mostly. On Windows I can implement a Directx version and an OpenGL version of every machine class and decide which one to use at runtime.
--> The file for the Common class can now contain all relevant data associated with it. In the previous implementation I was defining structures in a different file so that the Machine class could declare them. This meant that common data actually existed at the Machine level, just because the machine needed access to it. I didn't want the machine calling UP to the Common level because that just seemed wrong. But I never liked the way I had to implement it. Now this isn't an issue.

CON
--> All of these calls were not virtual previously which meant that the class had a smaller memory footprint and there wasn't the added overhead to the function calls. Plus inlining wasn't an issue.


The old implementation also had some potential issues if used improperly. For example if someone tried to use a pointer to a Machine class, things would have been bad. No function could call up to the derived class, and if you tried to delete a Machine pointer, but had allocated a Common class, there would have been a memory leak. These problems were apparent to me and I knew not to use the class in this way, but a good engine should be fool proof and usable by anyone, not just the person writing it ^_^.

3 Comments:

Anonymous AC Code Jockey said...

Uh oh, no you've done it. You've opened the can of worms. I went through this decision not too long ago. I don't have my notes on me right now... but I'll post later with some things. One thing for you though.... virtual functions on next-gen consoles is bad. They have a fairly large performance hit on the new processors, mainly due to no branch predictors. Granted you're working on PC, so it probably doesn't concern you too much.

1:07 PM EST  
Blogger keith said...

I'm interested to here more. It's good to know about the next-gen impact, since I'm not trying to target PC only. Ideally the engine should be portable to anything, especialy next-gen consoles.

So what model do you use then? The same as my current implementation, Machine as the base class?

Again I should mention that I don't plan on doing this for individual object classes, but rather what I call Engine classes. The engine will chug away on a lot of objects all batched up into only a few calls (Begin(); Draw(); End();) situations.

Let me know what you find in your notes.

3:04 PM EST  
Anonymous AC Code Jockey said...

Yeah, if you aren't calling these things a bunch per frame then the virtual calls won't be adding up too much.

One thing that I try to remember when I'm dealing with "virtualized" classes is that a reason for virtualizing it should be because you need to make a "run-time" decision about how that interface/class should work; typically the decision is compile based. You did cite an example of having a DX version and a OGL version of a class. Most of the time however, engine modules won't really need to make these type of run-time decisions. However game code, or higher level engine code will.
So what am I getting at. Well, what I've come to find is that there is no "one true" design to fit them all. I actually have a couple that I prefer to use. I've seen various implementations now, and I find myself picking from each when it seems more fitting.
For my engine modules I find myself using the Machine class method you've described. Some nice things about this method are: No virtuals, common classes are not bloated by machine specific information from another platform, "virtualized" behaviour is emulated. Also, if it is a class for cross platform (xbox, pc, ps2), then there is no need for a create/factory function. This is because the implementation is compile based (not run-time based) and all the platform specific classes use the same name "MachineClass".
Bad things are: It takes a little getting used to since it seems back-asswards. More likely to have less shared code (but that's not a failing of the design).Can be a little more work to maintain.
Other implementations:
- Platform specific class is a member variable of the Common class. Common API will then call into the platform class API via its member.
- Standard C++ hierarchy, no virtuals, base class calls up the chain via ((MachineClass*)this)

And then there are a host of virtual style of implementations, but they don't offer much beyond a traditional virtual implementation.

11:38 AM EST  

Post a Comment

<< Home