|
March 10, 2001
revised May 17, 2001
[ Introduction | Object
Model
Object | Class |
Messages
| Methods | Encapsulation
| Interface | Implementation
Inheritance | Multiple
Inheritance | Polymorphism | Abstract
Class | Genericity | Exceptions
Debugging | ISO C99
| Performances | Keywords
| Examples |
References
| Mailing List | ChangeLog
]
Introduction |
This paper presents some programming techniques that allow large projects based on ISO C89 to get the benefits of object oriented design. It doesn't intend to be a course on OOP techniques and assumes the reader to have a good knowledge of the C language. Since OOPC is based on the C++ Object Model, a good knowledge of C++ may help to understanding it better. These techniques may be useful for programmers who have not a C++ compiler for their architecture (calculators, small systems, embedded systems). It may also be useful for people who are disappointed by C++ compilers which do not behave like the norm says or even do not support all the C++ features or by C++ APIs that change from time to time. In fact, I don't know (at the revised date of this paper) any compiler which fully support the norm C++98. It is clear that the techniques presented here have not the pretension to replace C++, that is impossible without a cfront translator (OOPC uses only C macros and few C lines), but it provides enough to do serious OOP:
Object Model |
OOPC is based on the Object-Oriented Model described in this section and strongly inspired from the C++ Object Model. If you are not familliar or simply not interested by object model, you can skip this section although reading it may greatly help for the understanding of the behaviour of OOPC and even of C++.
This model is built around three different types available for each class plus a general RTTI (Run-Time Type Information) type:
object
(*) vtbl
(1) type_info (1)
+----------+ +----------+ +----------+ | __vptr | ---+-> | __info | -----> | __name | +----------+ | | __offset | | __class | |[data] | | +----------+ | __super | | | | |[methods] | | __extra | +----------+ | | | | __offset | | +----------+ +----------+ class (1) | +----------+ | | __vptr | ---+ | ctor() | | dtor() | | ator() | +----------+ |[methods] | | | +----------+ |
The methods in the previous representation can be classified as follow:
The __vptr in object points to the virtual table and allows to call object methods without knowing its type. This is the key point of the polymorphism mechanism. It also allows to reach the class RTTI data required for dynamic casting.
The __vptr in class also points to the virtual table and allows to bypass the polymorphism mechanism. It also allows classes to behave like objects (with some limitations) for object methods calls and RTTI purposes.
Object Model and Single Inheritance
The principle used for single inheritance is the structure mapping,
that means if object2 inherits from object1, the data
of object1 are at the beginning of object2, and therefore an object2
can directly be used everywhere an object1 is expected. But to
guarantee the invariance of data alignment through inheritance, object
data members must be encapsulated into a structure. Obviously, the presence
of the constant __vptr pointer, which may point to different vtbl,
inside the structure forbids bitwise-copy of objects which must be performed
member by member. The same technique is used to build virtual table of
object2
but the encapsulation is not required since all slots have the same size
(i.e. function pointers). RTTI structures are simply chained. Here follows
the equivalent model representation:
object2
(*) vtbl2 (1)
type_info2 (1) type_info1 (1)
+----------+ +----------+ +----------+ +----------+ | __vptr | ---+-> | __info | -----> | __name | +-> | __name | +----------+ | | __offset | | __class | ---+ | ... | |[data1] | | +----------+ | __super | | | | | | |[methods1]| | __extra | | | +----------+ | | | | __offset | | | |[data2] | | +----------+ +----------+ +----------+ | | | |[methods2]| +----------+ | | | | +----------+ class2 (1) | +----------+ | | __vptr | ---+ | ctor() | | dtor() | | ator() | +----------+ |[methods] | | | +----------+ |
Object Model and Multiple Inheritance
The principle used for multiple inheritance is the aggregation of objects,
that means if object3 inherits from object2 and object1,
the data of object1 are at the begining of object3, followed
by the data of object2 including its __vptr pointer.
Therefore an object3 can directly be used everywhere an object1
is expected, like for single inheritance, but it can also be used everywhere
an object2 is expected after a small offset adjustment to the
object2
address inside the object3. Since object2 is defined
as an encapsulated member of object3, it is very easy to know
its offset and &object3->object2 tells to the compiler to
do this offset ajustment. Here follows the equivalent model representation:
object3
(*) vtbl3 (1)
type_info3 (1) type_info1 (1)
+----------+ +----------+ +----------+ +----------+ | __vptr | ---+-> | __info | -----> | __name | +-> | __name | +----------+ | | __offset | | __class | ---+ | ... | |[data1] | | +----------+ | __super | | | | | | |[methods1]| | __extra | | | +----------+ | | | | __offset | | | | __vptr2 | -+ | +----------+ +----------+ +----------+ + ---+ +---> | __info2 | --+ |[data2] | | |__offset2 | | type_info2 (1) | | | + ---+ | +----------+ +----------+ | |[methods2]| +--> | __name | |[data3] | | | | | ... | | | | +----------+ | | +----------+ | |[methods3]| | | | | | | | | +----------+ +----------+ class3 (1) | +----------+ | | __vptr | ---+ | ctor() | | dtor() | | ator() | +----------+ |[methods] | | | +----------+ |
The RTTI of object3 and object1 are chained, but the RTTI of object2 remains separate since object2 inside object3 must behave exactly as a true object2. In fact, the only difference between a true object2 and the object2 inside object3 is the value of the field __offset2 in the virtual table which holds the offset of object2 inside object3. A true object2__vptr would point to the virtual table of object2 and not inside the virtual table of object3 and therefore the __offset field value would be zero.
For more information about the OOPC Object Model, see the
pseudo C file objectModel.c.
Object |
Assuming that a person may be identified by its name, this information should be encapsulated into an object to reflect the ownership of these data by the person. A typical approach would be:
struct t_person {In fact, objects and classes in OOPC are always defined as aggregations (structures or unions). It answers to the second constraint: the name encapsulation. OOPC macros achieve simply the object definition which in reality looks like:
char *name;
};
typedef union { /* generated */From this definition, which is exactly what OOPC does, we can do the following remarks:
struct _ooc_vtbl_person const*const __vptr; /* generated */
struct _ooc_vtbl_object const*const __iptr; /* generated */
struct { /* generated */
t_object const private(_); /* generated */
char const* private(name);
} m; /* generated */
} t_person; /* generated */
#define OBJECT personThe private() macro transforms the token of the field name into something which is unreachable outside from the class implementation (C++: private). The public() macro is also available to define public (reachable) fields (C++: public). In the BASEOBJECT_METHODS section, the object method (message) print is defined as a constant object method (C++: constant virtual member function) using the constMethod() macro, that means it does not modify the this pointer. The method() macro is also available to define non-constant object methods. Note that the class person does not derive from any other class and therefore is defined as a base object.BASEOBJECT_INTERFACE
char const* private(name); /* object member */
BASEOBJECT_METHODS
void constMethod(print); /* object method */
ENDOF_INTERFACE
Declaring an object instance, a pointer to an object instance, a constant pointer to an object instance and a constant pointer to a constant object instance can be done as in C++:
Class |
Classes collect and encapsulate the following information:
struct _ooc_class_person { /* generated */by the equivalent (which must follow the object interface, see person.h for a complete interface):
struct _ooc_vtbl_person const*const __vptr; /* generated */
t_person (*const person)(void); /* generated */
void (*const _person)(t_person *const this); /* defined */
t_person *const (*const alloc)(void); /* generated */t_person *const (*const new)(char const name[]);
void (*const init)(t_person *const this, char const name[]);} person; /* generated */
CLASS_INTERFACEHere, the declared variable person is the class itself of person while t_person has been previously defined to be the object type of person. The important difference appearing here is that we need only one class person (instance) while we need a lot of objects person (instances). The hidden type struct _ooc_class_person should never be used (no necessity).t_person *const classMethod_(new) char const name[] __;
void method_(init) char const name[] __;ENDOF_INTERFACE
In the class definition and declaration above, we make the difference between the class method and the method. The latter requires a t_object *const (i.e. t_person *const) as it first argument and this pointer is referenced as the this pointer inside the method (C++: this). If the method is a constant method (C++: constant member function), the this pointer will be of type t_object const*const. Class methods returning a t_object* are called objects factory.
The special _(args) args __ declaration is the mechanism used to extend the method macro to any number of arguments. Each time a token/keyword finishes with a _ it waits for the closure __. This artifact is not required if you have a C99 preprocessor (see ISO C99).
Note: If you don't know where to put your methods, you should remind
that classes should only contain class methods like new()
(object factory) and methods strongly related to the object type like init()
and copy(). All other methods should be considered as object methods.
If you have any doubts, use object methods, this will avoid any big changes
in future interfaces of derived classes. Moving a method from the object
definition to the class definition bring only slights changes (remove polymorphism).
Moving a method from the class definition to the object definition can
be a major task of a project (add polymorphism).
Messages |
In order to use person objects in our program, we still need to know how to send messages to an object. In this section we use the terminology sending messages (to an object) while in the Methods section, we use the terminology calling methods (of a class). We always send a message to an object by using the two commands sendMsg(object, message) and sendMsg_(object, message) args __:
t_person *const per = person.new("Brown");The first line creates (allocates and initializes) a new person per with some initialization arguments ("Brown"). As we have seen in the class definition, new is a class method and can only be called throughout its class. If object dynamic allocation fails, the exception ooc_bad_alloc is thrown (see Exceptions). The following line print the per object by sending the print message to the object per. The command delete() is a special macro which always delete properly an object by calling its destructor and then freeing the object instance. If you need a function pointer to delete() (see Exceptions), use ooc_delete() instead. Obviously, messages can only be sent to properly initialized objects. It is also possible to work with automatic objects to avoid dynamic allocation:
sendMsg(per, print);
delete(per);
t_person per[1] = { person.person() };The first line initialize a new person per using the object constructor person() automatically provided for each class (see Interface). Like for the new class method, the constructor must be called throughout its class. After this step, per is a well formed object, but not yet initialized and this is done by the second line where the method init is called to initialize the object. The following line print the per object by sending the appropriate message to the object per. Finally the object per is cleared but not deleted since it is not a dynamically allocated object (automatic object). Basically, each time you use an equivalent of new (resp. init) to create/initialize an object, you probably must use delete (resp. _object) to destroy this object before the end of its scope. init() is a method and not a message since it is strongly related to the object type and therefore will never be polymorphic.
person.init(per, "Brown");
sendMsg(per, print);
person._person(per);
In the example above, we have declared per as an array to avoid the & in the macros. If your compiler complains about the first line you can either do the initialization after the declaration or use the following alternative:
t_person per = person.person();Dealing with array of automatic objects may be a little more complex:
person.init(&per, "Brown");
sendMsg(&per, print);
person._person(&per);
int i;The use of memcpy() is required to cast away the constant specifier of __vptr to avoid any objects bitwise copy. To allow such objects copy (this assumes that you know what you do), you can compile your C files with the flag -DALLOW_OBJCOPY. Then the statement per[i] = per_ref; will become valid. The macro objCopy(obj1, obj2) will be substituted by the assignment statement (i.e. obj1=obj2) if -DALLOW_OBJCOPY is specified or else by memcpy.(i.e. memcpy(&obj1, &obj2, sizeof(obj1))). Obviously, this macro waits for objects instance, not objects addresses, to be able to compute objects size. If automatic object array manipulation is important, it would be better to write the initArr(size, args) and clearArr(size) methods for this purpose.
t_person per_ref = person.person();
t_person per[100];for(i=0; i<100; i++) {
memcpy(per +i, &per_ref, sizeof(t_person));
person.init(per +i, "Unkown");
}for(i=0; i<100; i++) {
sendMsg(per +i, print);
}for(i=0; i<100; i++) {
person._person(per +i);
}
Methods |
If you know the exact class of an object, sending a message can be replaced by the call of the object's method throughout its class like for the methods and class methods. For example,
sendMsg(per, print);is equivalent to
sendCMsg(per, person, print);But beware that you can involuntary call the wrong object method if for example it has been overloaded (see Polymorphism). That is why it is mainly reserved for the class implementation where usually objects types are well known and where using object methods through their class is the only way to bypass the polymorphism mechanism. Another safe solution is:
methodAddr(per, print)(per);where methodAddr() returns a constant pointer to the object's method, but per appears twice in the statement and it is not very elegant comparing to the use of sendMsg(). To be complete, the equivalent of the sendCMsg() used above would be:
methodAddr(&person, print)(per);Note: messages and methods are function pointers and therefore can be used as such.
Encapsulation |
Leaving objects members to public access can bring serious data integrity problem like changing the stored size of an array without adjusting the array size. One way to protect data and methods is to declare them as private. Private members can only be accessed by their class or subclasses (see Inheritance). So it is wise to declare private all members that should not be accessible by objects users.
Private specification can be applied to the object data and methods as well as to class data and methods. Sometimes, for simplicity or efficiency, it is also useful to have direct access to object members like for the complex number object:
#define OBJECT complexPublic and private members and methods can be intermixed without any problem. To reach a public member of an object, you do exactly as for structures:BASEOBJECT_INTERFACE
double public(real);
double public(imag);BASEOBJECT_METHODS
/* no virtual function */
ENDOF_INTERFACE
t_complex *const j = complex.alloc();
j->m.imag = -1;
Interface |
The interface is simply where object and class definition take place, usually in a header file (person.h) which looks like:
#ifndef PERSON_HThis file is split into two parts which describe the object and class definition as seen before. One new thing to note is the inclusion of the header file ooc.h which provides a set of useful keywords and macros like (BASE)OBJECT_INTERFACE, (BASE)OBJECT_METHODS, (ABSTRACT)CLASS_INTERFACE and ENDOF_INTERFACE (see Keywords). Macros XXX_INTERFACE delimit sections of the object and class interface definition. Another thing is the declaration of OBJECT as person which specifies the class name for the interface and the implementation and creates automatically the declaration t_OBJECT which is the generic name of the object type (i.e. t_person, see Genericity). We also find the print method which prints a person's name. Since print does not change the object state, it is declared as a constant object method.
#define PERSON_H#include <ooc.h>
#undef OBJECT
#define OBJECT personBASEOBJECT_INTERFACE
char const* private(name);
BASEOBJECT_METHODS
void constMethod(print);
ENDOF_INTERFACE
CLASS_INTERFACE
t_person *const classMethod_(new) char const name[] __;
void method_(init) char const name[] __;
void method_(copy) t_person const*const per __;ENDOF_INTERFACE
#endif
Once the person interface is properly defined and included into your file, the following things are available:
Warning: Do not confound the new() class method,
the allocator and the constructor! The constructor object()
returns a well formed default object copy with all fields set to zero plus
static initialization specified in initClassDecl() (see Implementation),
the allocator alloc() allocates an object and calls the constructor
and new() calls the allocator and init().
Implementation |
The implementation is the hidden side of the class. Only class designers are concerned by the class implementation which take place into a separate C file (person.c) with the same name as the class interface header file (person.h). A good advise would be to always design entirely the interface first and then to program the implementation. This should help you to concentrate on object data and methods which is the most important part of your class and to postpone technical problems at development time. It is time now to have a look to the implementation:
#include <stdio.h>The file starts by the definition of the keyword IMPLEMENTATION followed by the inclusion of the person interface. It is important to include person.h after the IMPLEMENTATION definition otherwise its content would be different and you would not be able to reach the object private member name or to use most of the keywords displayed in bold in the listing above.#define IMPLEMENTATION
#include <person.h>
void
constMethodDecl(print)
{
printf("name:\t%s\n", this->m.name);
}BASEOBJECT_IMPLEMENTATION
methodName(print)
ENDOF_IMPLEMENTATION
initClassDecl() {} /* class ctor, required */
dtorDecl() /* object dtor, required */
{
free((void*)this->m.name);
this->m.name = NULL;
}t_person
classMethodDecl_(*const new) char const name[] __
{
t_person *const this = person.alloc();
person.init(this, name);
return this;
}void
methodDecl_(init) char const name[] __
{
this->m.name = strdup(name);
}void
methodDecl_(copy) t_person const*const per __
{
person._person(this);
person.init(this, per->m.name);
}CLASS_IMPLEMENTATION
methodName(new),
methodName(init),
methodName(copy)ENDOF_IMPLEMENTATION
Then follows the object methods definitions. The use of the keyword methodDecl, constMethodDecl and classMethodDecl helps to declare correctly the methods, the constant object methods and the class methods. All methods have the storage class specifier static which guarantees the encapsulation of the methods into the implementation module (i.e. the file). Since the static storage specifier is included into the methodDecl, constMethodDecl and classMethodDecl macros, pointers qualifier * and const have to be put with the method name, not with the returned type (i.e. char methodDecl(*const getName);). A clean solution to avoid this little annoyance is to define the returned pointer type with typedef locally to the implementation. Macros methodDecl, constMethodDecl and classMethodDecl have their equivalent version with variable number of arguments: methodDecl_, constMethodDecl_ and classMethodDecl_.
Then follows the object implementation delimited by (BASE)OBJECT_IMPLEMENTATION and ENDOF_IMPLEMENTATION and inside these tags you find the list of the object methods in the same order as declared in the object methods interface. Whatever, the compiler will complain if the order is not respected (except if two successive methods have the same prototype!).
Then follows the required initClassDecl() declaration which is a special local function where class initializations are done like default object initialization (returned by the constructor), superclasses initialization (see Inheritance) or functions overloading (see Polymorphism). Default static object initializations can be achieved by assigning default values (by default all fields are set to zero) to members using objDefault() (i.e. objDefault(level) = 1; in manager.c). The destructor _person() declared by dtorDecl() is always required even if it is empty like initClassDecl() is this example.
Then follows the methods and class methods definitions. Again the method declarations are done with the methodDecl, constMethodDecl and classMethodDecl macros. The class method new looks 99% of the time like this one, that is calling the allocator and initializing the new object.
Finally, the class implementation is delimited by CLASS_IMPLEMENTATION and ENDOF_IMPLEMENTATION and inside these tags you find the list of the methods and class methods in the same order as declared in the class interface. Whatever, the compiler will complain if the order is not respected (except if two successive methods have the same prototype!).
The section (BASE)OBJECT_IMPLEMENTATION and (ABSTRACT)CLASS_IMPLEMENTATION can be declared at the top of the file if you provide the methods prototypes before. It may significantly improve the readability of big implementation.
Note: Inside methods and object methods, the current object is always called this (C++: member function and this pointer). It is a pointer to constant object if the method has been declared as a constant method (C++: constant member function).
Note: If your libc does not provide the non-C89 strdup() function,
your can use the one provided in ooc.c by compiling your programs
with the flag -DALLOW_STRDUP.
Inheritance |
Up to now, we have seen how to define base objects which do not inherit from other objects. But without inheritance you cannot reuse already implemented objects or split your project into smaller entities like OOP requires. Since we need at least another object, we create the specialized subclass employee from the class person. As in real life, an employee is a person to which we add a departement information. So we derive employee from person. Person viewed from employee becomes a superclass. Here is the employee interface (employee.h):
#ifndef EMPLOYEE_HThe listing above shows how it is simple to derive your class from one or more superclasses. First you need to include the interface of the superclass. Then you define the class name, the object interface and the class interface as we have seen before (see Interface). The inheritance is built by the declarations INHERIT_MEMBERS_OF() and INHERIT_METHODS_OF() in the object definition delimited by OBJECT_INTERFACE and OBJECT_METHODS. Inheritance declaration must always be placed at the beginning of these sections. As you can see, the BASE prefix disappeared from these tags since employee is not a base object but a derived object.
#define EMPLOYEE_H#include <person.h>
#undef OBJECT
#define OBJECT employeeOBJECT_INTERFACE
INHERIT_MEMBERS_OF (person);
char const* private(department);OBJECT_METHODS
INHERIT_METHODS_OF (person);
ENDOF_INTERFACE
CLASS_INTERFACE
t_employee*const classMethod_(new) char const name[], char const department[] __;
void method_(init) char const name[], char const department[] __;
void method_(copy) t_employee const*const emp __;ENDOF_INTERFACE
#endif
The inheritance declarations in implementation are as simple as for the interface (employee.c):
#include <stdio.h>The SUPERCLASS macro declares person as a superclass of employee. Like for the methods declarations, it must appear in the same order and place as in the beginning of the interface definition. The initClassDecl() which this time is not empty, is automatically called once by the constructor to properly initialize the superclass person and therefore guarantee the validity of the employee class. The rest of the file looks like the person implementation adapted for employee, it means that employee init() and _employee() class methods call first the person init() and _person() class methods. Finally the overloaded print object method of person (same slot in vtbl) calls the person print methodand display information related to employee only (i.e. the department). Since no new object method has been defined for employee, the size of the virtual table of employee is the same as the size of the virtual table of person.#define IMPLEMENTATION
#include <employee.h>
void
constMethodOvldDecl(print, person)
{
sendCMsg(this, person, print);
/* sub_cast() downcast this from person to employee */
printf("\tdept:\t%s\n", sub_cast(this,person)->m.department);
}OBJECT_IMPLEMENTATION
SUPERCLASS(person)
ENDOF_IMPLEMENTATION
initClassDecl() /* class ctor, required */
{
initSuper(person);
overload(person.print) = methodOvldName(print, person);
}dtorDecl() /* object dtor, required */
{
person._person(super(this,person)); /* upcast */
free((void*)this->m.department);
this->m.department = NULL;
}t_employee
classMethodDecl_(*const new) char const name[], char const department[] __
{
t_employee *const this = employee.alloc();
employee.init(this, name, department);
return this;
}void
methodDecl_(init) char const name[], char const department[] __
{
/* super() upcast this from employee to person */
person.init(super(this,person), name);
this->m.department = strdup(department);
}void
methodDecl_(copy) t_employee const*const emp __
{
employee._employee(this);
employee.init(this, emp->m.person.m.name, emp->m.department);
}CLASS_IMPLEMENTATION
methodName(new),
methodName(init),
methodName(copy)ENDOF_IMPLEMENTATION
The super macro (equivalent to super_cast()) used in the methods implementation above upcast this which is an employee (t_employee) to a person (t_person). Every time you need to send a message or call a method of a superclass, you must use super(object, superclass) to upcast the object into a superobject. Since a class may inherit from several superclasses (see Multiple Inheritance), the full member name (without the first m) of the superclass must be provided. To reach a superobject public members of an object, you do exactly as for normal class:
super(object, super)->m.public_member.The sub_cast() macro used in the methods implementation above downcastthis which is a person (t_person) to an employee (t_employee). Every time you need to reach the subclass in its implementation, you must use sub_cast(object, superclass) to downcast the superobject into the implemented object. Since the subclass may inherit from several superclasses (see Multiple Inheritance), the full member name (without the first m) of the superclass must be provided. To reach the subobject public members of an object, you can do:
sub_cast(object, super)->m.public_member.In fact, sub_cast() is only available in implementation and refers to the definition of OBJECT to know the subclass name.
To summarize, super_cast() (or super()) upcast a subclass
to a superclass while sub_cast() downcast a superclass to the
OBJECT
subclass. Both are static cast resolved a compilation time.
Multiple Inheritance |
Multiple inheritance can be easily achieved in the same way as single inheritance (see Inheritance) by duplicating approprialty INHERIT_MEMBERS_OF(), INHERIT_METHODS_OF(), SUPERCLASS() and initSuper() definition and declarations. Assuming the existence of the superclasses class1,class2 and of the class aClass which inherits from both superclasses. Starting from single inheritance, some modifications have to be done in interface:
#ifndef ACLASS_Has well as in implementation:
#define ACLASS_H#include <class1.h>
#include <class2.h>#undef OBJECT
#define OBJECT aClassOBJECT_INTERFACE
INHERIT_MEMBERS_OF (class1);
INHERIT_MEMBERS_OF (class2);
...
OBJECT_METHODSINHERIT_METHODS_OF (class1);
INHERIT_METHODS_OF (class2);
...
ENDOF_INTERFACE
CLASS_INTERFACE
...
ENDOF_INTERFACE#endif
...And that is all. As previously mentioned, superclasses declaration must be done at the beginning of each section of the object interface and must be at the same place in the object implementation. Sending a message to a superclass is identical to single inheritance:
OBJECT_IMPLEMENTATIONSUPERCLASS (class1),
SUPERCLASS (class2),
...
ENDOF_IMPLEMENTATIONinitClassDecl()
{
...
initSuper(class1);
initSuper(class2);
...
}
...
CLASS_IMPLEMENTATION
...
ENDOF_IMPLEMENTATION
sendMsg(super(object, superclass), message);If you inherit twice (two levels of inheritance), you should do:
sendMsg(super(object, superclass.m.supersuperclass), message);and so on. The advantage of specifying the superclass name is that you can choose which superclass receives the message (two superclasses may answer to the same message).
But it can become quickly boring to use super() each time you have to send a message to your superclasses, or you may want to send the same message to all your superclasses at the same time. In that case, the best thing to do is to declare in your aClass a method using the same name (i.e. the_message) with a declaration which should look like:
voidNow, assuming object to be of class aClass, sendMsg(object, the_message); in a program will send automatically the correct message to all your
methodDecl(the_message)
{
sendCMsg(super(this,class1),class1,the_message);
sendCMsg(super(this,class2),class2,the_message);
}
Polymorphism |
The polymorphism is the aptitude of an object to be used as another object while keeping its original behavior. This section assume the following inheritance hierarchy:
manager --> employee --> personwhere --> indicates the class derivation. For a complete interface and implementation of these objects see person.h, person.c, employee.h, employee.c, education.h, education.c, manager.h and manager.c.
--> education
One important thing in these classes is the overload of their print method, like for the manager example:
voidThis overloading mechanism change the manager virtual table slot employe.person.print which initially points to the person print message, to pointing to the print message defined above. This new message call the print message of the employee and education classes using sendCMsg() to avoid infinite loop and then print information related to manager. Inside implementation, as you can see, you can change from one superclass to another by using a combination of sub_cast() and super_cast(). Outside implementation you have to either use static_cast(this, employee.m.person, manager) (unsafe) and use super_cast() to get the superclass or to use dynamic_cast(this, education). The latter is slower but safe since it behaves exactly as the C++ dynamic_cast operator.
constMethodOvldDecl(print, person)
{
/* this is a person */
/* send the print person message to this as an employee */
sendCMsg(this, employee, person.print);
/* send the print person message to this as an education */
sendCMsg(super(sub_cast(this,employee.m.person),education),education,print);
printf("\tlevel:\t%d\n", sub_cast(this,employee.m.person)->m.level);
}...
initClassDecl()
{
initSuper(employee);
initSuper(education);
overload(employee.person.print) = methodOvldName(print, person);
...
}
The following example shows how to display all information of a person whatever it is a person, an employee or a manager (see test_manager.c for a complete example). The small program:
#include <manager.h>will gives the output:void print_person(t_person *const per)
{
sendMsg(per, print);
}int main(void)
{
t_person *const per = person .new("Brown");
t_employee *const emp = employee.new("Smith", "Cars");
t_manager *const mng = manager .new("Collins", "Trucks", "PhD", 2);print_person(per);
print_person(super(emp,person));
print_person(super(mng,employee.m.person));delete(per);
delete(emp);
delete(mng);return EXIT_SUCCESS;
}
name: Brownand we see that the function print_employee() does not need distinguish a person from an employee or a manager. The polymorphism is achieved by the message print in the function print_person. Using sendMsg() always ensures to refer to the right virtual table (through __vptr) and therefore to call the appropriate object method.
name: Smith
dept: Cars
name: Collins
dept: Trucks
dipl: PhD
level: 2
To simplify polymorphism implementation, ooc.h also provides few interesting macros:
Abstract Class |
Abstract classes are commonly used to defined a common object interface for a set of objects. They rarely implement the services they declare (set to zero). These services must therefore be implemented by the subclasses and message passing resolution will be done by the polymorphism mechanism. Abstract classes are classes without object factory like alloc() or new() or with an incomplete set of messages. Pure abstract classes are classes without implementation (not supported by OOPC).
The abstract class interface must be declared with ABSTRACTCLASS_INTERFACE
and the implementation must be declared with ABSTRACTCLASS_IMPLEMENTATION.
The allocator (alloc()) is not available and therefore is it not
possible to implement object factory (see memBlock in Examples).
Constructor (object()) and destructor (_object())
are still available.
Genericity |
Genericity can partially be performed with polymorphism at the level of object or with a combination of untyped pointer (void*) and isA() tests at the level of functions. But to write objects independently of data types like the C++ allows with templates, we need to introduce some programming techniques. The principle is based on the use of defined generic types like gType1, gType2, etc... in place of specialized types in objects and classes interfaces and implementations. Specialization of objects and classes is postpone at the user level according to its needs.
To simplify the transition between specialized code and generic code you can follow the step by step procedure below. Examples are based on the generic memBlock and array (which inherit from memBlock) classes (see Examples):
Generalization (designer)
Exceptions |
An exception is a mechanism for handling errors in a different way than returning special values or setting global variables. The exceptions use the same philosophy as OOP techniques, that means solving the problem where you have the knowledge for. The advantages are on the object user side, you do not care where exceptions (errors) are trigged and therefore you do not need to check all the values returned by the called functions. You may catch the exceptions in your program and solve the problem at the appropriate place. On the object programmer side, you don't care how to answer to an exception and where to go after. You just throw the exception (error) and it is up to the user to catch it or not.
To use exceptions, you need to include the header file exception.h and to link the file exception.c in your project. Then exceptions are ready to use more or less like in C++:
#include <stdio.h>If div_() or ln_() throw an exception, it will automatically be caught in the main() function and there is no needs to test the external errno variable. The advantages is that div_() or ln_() could have been called by a lot of intermediate functions without any change needed in the exception handling. Note that if main() would have been an intermediate level function, the line printf("unknow exception\n"); would have been advantageously replaced by throw(exception); in order to propagate any unprocessed exception. If a try, catch() and catch_any blocks do not contain variable declaration, the braces are optional (like for the cases in the switch() control statement).
#include <math.h>
#include <exception.h>/* can also be #defined */
enum { no_exception, zero_divide, domain_error, bad_alloc } exceptions;double div_(double a, double b)
{
if (b == 0.0) throw(zero_divide);
return a/b;
}double ln_(double a)
{
if (a <= 0.0) throw(domain_error);
return log(a);
}int main(void)
{
double a, b;printf("a = "); scanf("%lf", &a);
printf("b = "); scanf("%lf", &b);try {
printf("ln(%g/%g) = %g\n", a, b, ln_(div_(a,b)));
}
catch(zero_divide) {
printf("zero division\n");
}
catch(domain_error) {
printf("domain error\n");
}
catch_any {
printf("unknow exception %d\n", exception);
}
endtry;return EXIT_SUCCESS;
}
Throwing an exception while no try{} appears at a higher level is equivalent to call exit(exception). If an exception is not caught within a try{} ... endtry statement, the process continues just after the endtry (C++: exception behavior).
The following OOPC exceptions are already declared in ooexception.h:
Throwing an exception can bring dangerous side effects for dynamically allocated memory between the try and the throw statements. To protect memory allocation against exceptions, you may protect them (C++: auto_ptr) in your intermediate functions:
t_employee *const emp = employee.new("Brown", "Cars");Protection must take place just after pointer declaration and assignation. It needs to be in the declaration part of the block since it also does declarations of hidden variables to manage the stack of protected pointers.
protectPtr(emp, ooc_delete);
...
/* an exception can be thrown here, emp will be safely deleted */
...
unprotectPtr(emp);
delete(emp);
Unprotection must take place before the end of the scope of the protected pointer, but it can be after the freeing of the memory the pointer was pointing to.
Exception handling and pointer protection do not make any dynamic allocation/freeing, so throwing a bad_alloc exception is safe.
Warnings: protection and unprotection must be done in
reverse
order (think about something like push(ptr) and pop(ptr)).
Unprotecting a pointer does not free the memory it points to. Unprotecting
a pointer which is not on the top of the stack is safe, therefore unprotecting
the same pointer twice is safe. Throwing an exception in the pointer_free
function gives an undefined behavior (like in C++). The function
ooc_delete
doesn't throw any exception.
Debugging |
OOPC provides four kinds of debbuging facilities: tracing function calls may help in debugging polymorphic methods, tracing exception thrown may help in debugging uncaught exception, tracing memory allocation may help in debugging object management and finally tracing object construction may help in understanding OOPC or find bugs in object hierachy. The file ooc.h includes automatically the header oodebug.h if one of these debugging facilities is activated, but you still need to add the file oodebug.c to your project.
Function call
To debug function call, add DEBUG_VOID_PROTO or DEBUG_PROTO at the beginning of the arguments list of functions prototypes and declaration and add DEBUG_VOID_ARGS or DEBUG_ARGS at the beginning of the arguments list of functions calls. The VOID versions are for functions which normally have no arguments. To trace functions which have XXX_PROTO and XXX_ARGS definition and calls, add DEBUG_DISPCALL(file, string) in their core where file can stderr and string any message.
Then compiling your project with the flag -DDEBUG_CALL will activate the function call debugging and will give an output like:
file(line):calling_function - called_function:messagefor each call of a traced function. This is helpful to trace messages.
Exception throw
Compiling your project with the flag -DDEBUG_THROW will activate the exception throw debugging and will give an output like:
file(line):throwing_function: exception 'exception' (id #) thrownfor all thrown exception. This is helpful to trace uncaught exceptions.
Memory allocation
Put DEBUG_DISPMEM(file) where file may be stderr in your program, usually before exit points and compile your project with the flag -DDEBUG_MEM to activate the memory allocation debugging. If you have any memory leaks, you will have an output which will look likes:
Index Address Size Begin End File(Line) - Total size 10This is a memory status report where you find the pointer index, the pointer value, the memory size allocated, the beginning of the memory bloc (4 characters), the end of the memory bloc (4 characters) and the location file:line:function where the pointer has been allocated. If you try to free a corrupted memory bloc, you will have an output which will look likes:
0 0x804a828 10 .... .... test_protection.c(41):div2_
Heap corrupted @ beginning of 0x804a848 - test_protection.c(48):div2_The first two lines report that a memory corruption has been detected at the beginning and at the end of the memory bloc. The last two lines are the memory status report were the pointer 0 is now tagged as INVALID.
Heap corrupted @ end of 0x804a848 - test_protection.c(48):div2_
Index Address Size Begin End File(Line) - Total size 10
0 0x804a848 10 .... .... test_protection.c(41):div2_ **INVALID**
You can also check for a memory bloc validity with memchk(pointer, file). If bloc is valid, nothing is displayed otherwise you get corruption error messages and the returned value -1.
Note 1: Debugging protected pointers (see Exceptions) may cause some problems since the exception handler does not provide debugging information to the free function. Therefore, you might use a wrapper to the free function as shown in the test_protection.c file.
Note 2: Since oodebug.h wraps dynamic allocation functions like malloc() and strdup() with macros, it must always be included after the standard headers stdlib.h and string.h. The same remark applies to ooc.h since it automatically includes oodebug.h if you specify -DDEBUG_MEM.
Object construction
To see the construction of an object or a class in memory, compile your project with the flag -DDEBUG_OBJ to enable debugging of objects and classes construction. Then using ooc_printObjInfo(file, object) and ooc_printClassInfo(file, object) you can display the components of an object or a class. In test_manager.c, ooc_printObjInfo(stdout, mng) will print out something like:
OBJECT manager @ 0x804b258while ooc_printClassInfo(stdout, mng) will print out something like:
ctor @ 0x804afc0
info @ 0x804afe0
vtbl @ 0x804b004
class @ 0x804b01c
SUPEROBJECT employee @ 0x804b258
base @ 0x804b258 (offset = +0)
info @ 0x804aee0
SUPEROBJECT person @ 0x804b258
base @ 0x804b258 (offset = +0)
info @ 0x804ae60
SUPEROBJECT education @ 0x804b264
base @ 0x804b258 (offset = +12)
info @ 0x804af60
CLASS manager @ 0x804b01c
ctor @ 0x804afc0
info @ 0x804afe0
vtbl @ 0x804b004
SUPERCLASS employee @ 0x804af10
ctor @ 0x804aec0
info @ 0x804aee0
vtbl @ 0x804af04
SUPERCLASS person @ 0x804ae90
ctor @ 0x804ae40
info @ 0x804ae60
vtbl @ 0x804ae84
SUPERCLASS education @ 0x804af90
ctor @ 0x804af40
info @ 0x804af60
vtbl @ 0x804af84
ISO C99 |
ISO C99 provides new interesting features like the __VA_ARGS__
predefined macro which simplify the use of macros with variable number
of arguments. Using __VA_ARGS__ allows to replace sendMsg_(obj,
msg)
args
__; by sendMsg_(obj, msg, (args)); which
may be considered to be closer to the C grammar. I still prefer to group
args
into parenthesis to be homogeneous with the sendMsg(obj,
msg);
command. This change can be applied to all the OOPC commands ending by
an underscore (see ooc99.h).
Performances |
Methods calling and messages sending speed efficiency is
more or less the same as in C++ since the programming techniques used behind
are very close. So in general, you will nearly get the same performance
as in C++ without inlining (+/- 10%). Object instance size is exactly
the same as its C++ equivalent without drastic size optimisation.
Keywords |
The list of introduced keywords at the preprocessor level is given in
the following table:
Interface
Encapsulation
Declaration
Messages
Miscellaneous
|
Implementation
Initialisation
Declaration
Miscellaneous
+ INTERFACE keywords
|
Generated class members
Required class members
Global names
Global functions
|
try {}
GENERICITY t_OBJECT (generic object type)
Required defines
|
Examples |
ISO C89
References |
Links given below can be read for information but they are not required to understand this paper since they do not follow the same philosophy. In fact, I disagree with most of the techniques presented into these references:
[1] La programmation par objets en langage C, by A. Gourdin, Technique et Documentation 1991List of C++ references
[2] Reusable Software Components, by Truman T. Van Sickle, Prentice-Hall 1996
[3] Object-oriented programming in C, by Paul Field, November 1991
[4] Object-oriented programming using C, by Dave St. Clair, October 1995
[5] Object Technology with C, by Paul Long, September 1995
[6] Object Orientated Programming in ANSI-C, by Axel Schreine, October 1993Some references of the C/C++ Users Journal
[7] Object-Oriented Programming As A Programming Style, by E. White, February 1990
[8] Object-Oriented Programming in C, by D. Brumbaugh, July 1990
[9] Creating C++-Like "Objects" in C, by C. Skelly, December 1991
[10] OOP Without C++, by B. Bingham, T. Schlintz and G. Goslen, March 1992
[11] Extending C for Object-Oriented Programming, by G. Colvin, July 1993
[1] The C++ Programming Language, 3rd edition, by Bjarne Stroustrup, Addison Wesley 1997
[2] The ANSI C++ Specifications (Draft), December 1996
[3] Annotated C++ Reference Manual, by M. A. Ellis and B. Stroustrup, Addison Wesley 1990
[4] Effective C++, 2nd edition, by Scott Meyers, Addison Wesley 1997
[5] More Effective C++, by Scott Meyers, Addison Wesley 1997
[6] Inside the C++ Object Model, by Stanley B. Lippman, Addison Wesley 1996
[7] Essential C++, by Stanley B. Lippman, Addison Wesley 2000
[8] Exceptional C++, by Herb Sulter, Addison Wesley 2000 (see also Guru of the Week)
[9] Modern C++ Design, by Andrei Alexandrescu, Addison Wesley 2001 (see also Loki)
[10] Advanced C++, by James Coplien, Addison Wesley 1992
[11] Secrets of C++ Master, by Jeff Alger, AP Professional 1995
[12] C++ Primer, 3rd edition, by Stanley B. Lippman, Addison Wesley 1998
[13] Scientific and Engineering C++, by John J. Barton and Lee R. Nackman, Addison Wesley 1994
[14] C++??: A Critique of C++, by Ian Joyner, October 1996
Mailing List |
If you are interested by updates and discussions about Object Oriented Programming in C, you can subscribe to the public mailing list forum-oopc by sending an e-mail to the CERN Listbox Server with the following content (subject is ignored):
subscribe listname your-email-address
ChangeLog |