I guess what I'm going to have to do is require the game to give me a creator function pointer that knows how to make all the objects. eg. it would give me something like
void * (*fp_factory) (int objectType); and fp_factory would point to something like void * GameObjectFactory(int type) { switch(type) { case OBJECT_PLAYER : return (void *) new Player; case OBJECT_ENEMY: ... } }Or something. As a game developer I hate that kind of global registry, because when you add a new object type you have to remember to go update this global registry file, which becomes a frequent merge disaster for source control, etc. I really like having everything you need to make a game object in a single CPP file. That means objects should be self-registering.
The way I usually do self-registering objects is with a little class that runs at cinit. Something like :
#define IMPLEMENT_FACTORY_PRODUCT(classname) namespace { ClassFactory::Registrar classname##registrar( classname::Factory , typeid(classname) ); } then in Player.cpp you have : IMPLEMENT_FACTORY_PRODUCT(Player);That's all fine and dandy, but it's not so cool for me as a library maker.
For one thing, doing work during CINIT is kind of naughty as a library. I've been trying to make it a rule for myself that I don't use object constructors to do cinit work the way I sometimes like to do. It's a perfectly normal thing to do in C++, and if you are writing C++ code you should make all your systems instantiate-on-use like proper singletons so that CINIT objects work - BUT as a library maker I can't require the clients to have done all that right. In particular if I do something like allocations during CINIT, they might run before the client does its startup code to install custom allocators or whatever.
For another thing, there are problems with the linker and cinit in libraries. The linker can drop objects even though they are doing cinit calls that register them in global factory databases. There are various tricks to prevent this, but they are platform dependent and it is a little evil bit of spaghetti to get the client involved in.
I guess I probably also shouldn't rely on "typeid" or "dynamic_cast" or any other runtime type information existing either since people like to turn that off in the compiler options for no good reason (it has basically zero cost if you don't use it). So without that stuff I pretty much just have to rely on the game to give me type info manually anyway.
Bleh, I'm just rambling now...
You could use the CINIT stuff purely to make a linked list of descriptor objects - no real "work", just a list of stuff to do. Then have the app call a proper MyLibraryInit() function which traverses the list of work-to-do and does the actual work that needs doing - registration etc. As it goes, it cleans up all the CINIT stuff.
ReplyDeleteOK, it's not really that much of an improvement.
The zero-cost argument if you don't use it is not correct for at least one compiler/linker pair for a portable platform (Which adds a lot of garbage to the linked executable that you can't easily remove). And it's actually quite common to remove unreferenced global objects. What you can do to prevent that is to reference them from a (generated) cpp file (that works for our unit-tests on all compilers we use)
ReplyDeleteI know exception handling in some compilers adds a lot of overhead even if you never throw, but how does RTTI bloat the exe? Sure there's a bit of information per type, but normally the number of different types (with vtables) in a project is very small. Yeah it's more than zero, but with MSVC anyway it's rarely bigger than few kb.
ReplyDelete