Reputation: 592
I have a project that must be in C (just to avoid the use C++ arguments).
The project depends on virtual tables and pointers to implement polymorphism.
Im stuck however in implementing super constructors from multi-level inheritance.
An example structure is:
Base Object
/\ /\
Constructed Object Simple Object
/\
SpecificConstructed
All objects have a name and a class. Constructed objects may have a list of sub objects for example. As simple object may only add a single value.
Base Object is just defined as:
struct _object {
struct _class *class;
char *name;
}
Class is where the virtual table is:
struct _class {
struct _class *super;
char *name;
size_t size;
void* (*init)(void *_this, char *name);
...
}
A constructed object is:
struct _constructed_object {
struct _object base;
void* components; //a list of sub objects for example
}
A sample simple object is:
struct _simple_object {
struct _object base;
unsigned char value; //a simple value for this specific type
}
So every object has a class, and classes can have supers, specially for the SpecificConstructed -> Constructed.
The definitions i have:
struct _class base = {0, "Base", sizeof(struct _object), base_init};
struct _class constructed = {&base, "Constructed", sizeof(struct _constructed_object}, constructed_init};
struct _class specific = {&constructed, "SpecificConstructed", sizeof(struct _constructed_object), specific_init};
struct _class simple = {&base, "SimpleOBject", sizeof(struct _simple_object}, simple_init};
This definition allows me to create objects of specify classes using a function:
new(struct _class *a_class) {
...
struct _object *o = calloc(1, a_class->size);
o->class = a_class;
o = a_class->init(o);
return o;
}
The idea is if i do:
new(SpecificConstructed)
New would create the appropriate space (sizeof(struct _constructed_object)), it would call "specific_init", which in turn would call "constructed_init" (it's super), which finally would call "base_init" (it's super). However the flow is specific_init, constructed_init, specific_init, constructed_init ...
The function i have for calling the supers initializer:
void* super_init(void* _this, char *name){
struct _object *o = (struct _object*)_this;
const struct _class *c = o->class;
const struct _class *s = c->super;
return (s && s->init) ? s->init(_this, name) : _this;
}
The simple (to - super) base method call works since i can just call the supers init.
But for the specific constructed, calling super takes me to the constructed object which is the correct step, but then instead of the constructed sending me up to the base_init, it sends me back to the specific_init call. This happens since I'm passing the same _this object which starts with the class specific i understand that, but not sure how to fix it and if its actually possible to fix?
Ive read the Object Oriented C book, but it deals with one-level inheritance Circle->Point, and the Metaclasses chapter just flew over my head. Ive also looked at the Objective-C runtime to see how that handles it, but it also has metaclasses and that i can't comprehend at the moment.
Upvotes: 2
Views: 133
Reputation: 62045
This is awesome stuff, I did a bit of that kind of stuff in C in the early nineties, before moving on to C++.
Unfortunately, despite the fact that your question is quite long, it is still a bit vague because it is not showing us certain things, like what is a "constructed object", (why are you calling it like that,) what is the difference between "constructed object" and "simple object", and what is "the simple->base method call". Also, why the size
? Also, I think that some sample code showing the actual problem with the invocation of the constructors is necessary.
The one thing that I can tell you right now about this design is that it strikes me as odd that you are storing a pointer to the constructor in the Virtual Method Table. In all object oriented languages that I know, (C++, Java, C#) constructors are never virtual; they look a lot more like "static" methods, which in C parlance are just plain link-by-name methods. This works fine, because every class has built-in, absolutely certain, unalterable knowledge of who its base class is.
Anyhow, chained constructor invocation is supposed to happen like this:
void basemost_init( struct basemost* this, char* name )
{
this->class = &basemost_class;
this->name = name;
...
}
void intermediate_init( struct intermediate* this, char* name )
{
basemost_init( &this->base, name );
this->class = &intermediate_class;
...
}
void descendant_init( struct descendant* this, char* name )
{
intermediate_init( &this->base, name );
this->class = &descendant_class;
...
}
Edit (after some clarifications)
If you want it to look cool at the allocation end, perhaps try something like this: (I am not sure how well I remember my C syntax, so please excuse any minor inaccuracies.)
struct descendant* new_descendant( char* name )
{
struct descendant* this = malloc( sizeof struct descendant );
descendant_init( this, name );
return this;
}
This way, you don't need a size
anymore. Also, note that you can pass as many constructor arguments as you want, without being restricted to a fixed, predetermined number of arguments, (which I consider to be extremely important,) and without having to use variadic constructors.
You may also be able to achieve the same thing with a #define
macro for all classes, if you promise to use consistent naming, so that the name of each constructor can always be computed as structname##_init
, but I am not sure how to pass arbitrary constructor parameters after the this
pointer through a macro in C. Anyhow, I prefer to avoid macros unless they are absolutely necessary, and in this case they are not.
Upvotes: 1
Reputation: 155206
super_init
can't work like that, it needs class on which to call super, otherwise (as you discovered) the immediate superclass constructor ends up calling itself over and over. Since each class knows its parent, it can call superclass's init directly. For example, simple.init
will call specific.init
, which will in turn call constructed.init
, and so on.
If you insist on a function to do that for you, you will have to give it the class so it can (trivially) invoke the constructor of the superclass. super
in Python 2 is an example of such a design. Python 3 introduces a simpler-to-use super
, but it required support from the compiler to figure out the correct class to pass to the underlying function.
Upvotes: 1