C++ design question

If you have any questions on programming, this is the place to ask them, whether you're a newbie or an experienced programmer. Discussion on programming in general is also welcome. We will help you with programming homework, but we will not do your work for you! Any porting requests must be made in Developmental Ideas.
Post Reply
BlackAura
DC Developer
DC Developer
Posts: 9951
https://www.artistsworkshop.eu/meble-kuchenne-na-wymiar-warszawa-gdzie-zamowic/
Joined: Sun Dec 30, 2001 9:02 am
Has thanked: 0
Been thanked: 1 time

C++ design question

Post by BlackAura »

My current approach to this is unbelievably ugly, so presumably there's a better way...

I want to write a program in C++, which can run on multiple platforms. Certain components (video, sound, input...) are platform dependent, so I need to implement different versions of the appropriate back-end code. In other words, I need to be able to swap out the back-end classes and replace them.

I can't simply have the back-end classes defined in a single header file, with different implementations in different files. That would work if no implementation required any additional member variables, but more often than not they do.

Each group of platform specific classes are quite tightly coupled. For example, an OpenGL-specific texture object can only really interact with other OpenGL-specific objects. For example, the core renderer might be a friend of the texture class, so it can directly access platform-specific information about the texture (like a PVR pointer, or an OpenGL texture ID). The renderer needs this information, and there's no way to move it around. In this case, making a base texture class and deriving platform-specific implementations from it doesn't make any sense. There's only ever going to be one implementation for each platform, you aren't going to be able to swap between different implementations on the fly, and if you were you would have some serious issues with type safety (or lack thereof).

So that leaves me with my current implementation, which is a bloody mess. There are two separate copies of the class header (and thus, two copies of the class definition), and two completely separate implementations of each class. They share an API, but that's all. They are otherwise completely separate, any shared code or interfaces must be manually copied across, and it's generally a right pain in the backside.

So, is there any clean solution to this, or am I going to have to put up with the rather disturbing mess that I've got?

Ideally, I'd like to be able to have a single definition for a class that works across all platforms, without using too many #ifdefs (preferably none at all), and still being able to share parts of the implementation where it makes sense to do so (although there's not much that can be shared in this case).
Strapping Scherzo
DC Developer
DC Developer
Posts: 2285
Joined: Fri Feb 21, 2003 7:37 am
Location: Chicago, IL
Has thanked: 0
Been thanked: 1 time
Contact:

Post by Strapping Scherzo »

I think in order to avoid the copying-functionality-between-implementations problem that you have right now, you have to go the #ifdef route. However, you can do that and still keep things clean.

Yes, you have to ifdef off all the platform specific member variabled for these classes. That's not too bad. What gets ugly is the functions where you have long blocks of code that are platform specific. I hate #idef blocks where I can't see the beginning and end of it in the same screen. You can cure lots of that with well written macros. But it's still going to be a little hairy, but better than your current method. You still get to keep a single copy of the program logic.

I think what's most important is to have the code, at compile time, be compiled as if you had written it only for that platform. I wouldn't use anything like class inheritence or purely virtual classes to accomplish it. That's great if you're switching out renderers at runtime, but not for what you're doing. Speed-wise, your current method or ifdefs is the best.
Image
BlackAura
DC Developer
DC Developer
Posts: 9951
Joined: Sun Dec 30, 2001 9:02 am
Has thanked: 0
Been thanked: 1 time

Post by BlackAura »

Ah, I was afraid of that. The only alternative I could see was inheritance or virtual classes, which I really do not want to do. Aside from still having duplicate definitions sprouting out all over the place, the extra level of indirection everywhere would be un-good.

So, we're back to stuffing the header files full of #ifdefs and macros, and trying not to mess up the implementation too much. At least I can keep most of the actual code clean.

Thanks!
Vorrtexx
Insane DCEmu
Insane DCEmu
Posts: 138
Joined: Sun Apr 06, 2003 5:29 am
Has thanked: 0
Been thanked: 0
Contact:

Post by Vorrtexx »

Interfaces combined with #ifdef statements might help?

How about defining the interface required, then create 2 seperate classes, both implementing the Interface, but with platform specific implementations of the interface.

When you need to define which class to use, an #ifdef could be used with an interface pointer to the instance.

That interface pointer could be used to call the methods of the interface implemented in the class then. and the pointer should be compatible with both implementations of the class.

sorry if I didn't make any of that clear....In a rush. Hope it helps.
Strapping Scherzo
DC Developer
DC Developer
Posts: 2285
Joined: Fri Feb 21, 2003 7:37 am
Location: Chicago, IL
Has thanked: 0
Been thanked: 1 time
Contact:

Post by Strapping Scherzo »

Vorrtexx wrote:Interfaces combined with #ifdef statements might help?

How about defining the interface required, then create 2 seperate classes, both implementing the Interface, but with platform specific implementations of the interface.

When you need to define which class to use, an #ifdef could be used with an interface pointer to the instance.

That interface pointer could be used to call the methods of the interface implemented in the class then. and the pointer should be compatible with both implementations of the class.

sorry if I didn't make any of that clear....In a rush. Hope it helps.
That extra level of indirection will hurt performance because every function call requires a lookup in the vtable for that class. Interfaces (purely virtual classes) are great if there is more than one implementation of that interface at runtime. But, as BA said, that's not the case. He would never have the DC rendering code compiled with the DirectX rendering code, nor could he. The class to which an interface pointer will point to will never change, so it's unecessary.
Image
quarn
DC Developer
DC Developer
Posts: 80
Joined: Wed Oct 17, 2001 7:44 pm
Location: Sweden
Has thanked: 0
Been thanked: 1 time

Post by quarn »

scherzo wrote:That extra level of indirection will hurt performance because every function call requires a lookup in the vtable for that class.
Though it would most probably not be the performance bottleneck.
scherzo wrote:The class to which an interface pointer will point to will never change, so it's unecessary.
Though it would give more readable code.
BlackAura wrote:In this case, making a base texture class and deriving platform-specific implementations from it doesn't make any sense.
I agree, the data layer should not change with the implementation, but the control-classes sometimes must implement the common interfaces differently. For the example with textures, I would give each texture an "int id;"-variable and implement the loading, setting of current active texture, disposing, etc in a singleton TextureManager-class. That class might use the "id" as a pointer to a pvr_poly_hdr_t, index into some array or an opengl texture id. It's up to the implementation of that class.

The same idea could be adapted to sound, 3d objects and other areas also.
BlackAura
DC Developer
DC Developer
Posts: 9951
Joined: Sun Dec 30, 2001 9:02 am
Has thanked: 0
Been thanked: 1 time

Post by BlackAura »

I currently have a single definition of each class, with platform-specific stuff (like pointers to SDL objects, PVR pointers, handles, whatever) #ifdefed out appropriately. There are three implementation files - one for platform independent code, and one for each supported platform. Some of the classes have functions that implement OS-specific stuff, and they're called at the appropriate time from the platform independent stuff. It's still a little messy, but it's functional.
nymus
DC Developer
DC Developer
Posts: 968
Joined: Tue Feb 11, 2003 4:12 pm
Location: In a Dream
Has thanked: 5 times
Been thanked: 6 times

Post by nymus »

I may not have properly understood the problem, but I would suggest using templates. My suggestion:

If each of the platforms uses a unique set of variables and procedures, then you have to write the code individually for each of them. The trick is to factor out the common functions and then base your template interface using these eg

Code: Select all

/* 
        common interface for all classes. used in the main part of the program 
        and dictates the functions which the backend(s) should provide
*/
template<class renderer_>
class engine {
public:
        engine(<common params>) {
                //init _engine_renderer;
        }
public:
        void                        draw_poly(const params& p) {
                // now all classes passed as a template must provide this func
                _engine_renderer.draw_poly(p);
        }

        void                        texture_poly(const params& p) {
                // now all classes passed as a template must provide this func
                _engine_renderer.texture_poly(p);
                // now all classes passed must contain this static variable
                if(_engine_renderer::poly_count > 900000) {
                }
                //etc
        }
private:
        renderer_                        _engine_renderer;
};
// now we can use the above interface to create renderes for different systems

Code: Select all

class opengl_renderer {
public:
        void                                draw_poly(const params&);
        void                                texture_poly(const params&);
private:
        //opengl-specific state
};
The concept of a poly and a texture should be kept common as possible throughout the entire program and each backend should hack the concept to make it fit in with its native structure

// now we can construct our prog without worrying about backends
//game.hpp

Code: Select all

template<class renderer_>
class game {
public:
        // develop game using an abstract gfx engine
private:
        engine<renderer_>        _gfx_engine;
};
// Time to select engine for specific platform

// posix_main.cpp

Code: Select all

#include "opengl_renderer.hpp"
#include "engine.hpp"
#include "game.hpp"

int main(int argc, char* argv[]) {
        //getopt
        game<opengl_renderer>        my_game(params);
}
// windows_main.cpp

Code: Select all

#include "directx_renderer.hpp"
#include "engine.hpp"
#include "game.hpp"

int main(int argc, char* argv[]) {
        //getopt
        game<directx_renderer>        my_game(params);
}

If I understood the problem, if my approach does not overestimate the capabilities of c++ templates, if I actually have experience writing real-world apps (like you do) then I wouldn't have to worry about #ifndefs et al.
behold the mind
inspired by Dreamcast
BlackAura
DC Developer
DC Developer
Posts: 9951
Joined: Sun Dec 30, 2001 9:02 am
Has thanked: 0
Been thanked: 1 time

Post by BlackAura »

Ah, neat. That'd probably work quite well.
Post Reply