Object Oriented Programming in C

Laurent Deniau

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:

OOPC also provides mechanisms to handle easily: Comparing to the C++ Object Model, OOPC does not provide: Beside the advantages of the OOPC, one important constraint is to keep type checking everywhere to ensure code quality and good debugging and therefore it forbids the extensive use of untyped pointers (void*) for polymorphism purposes. Another constraint is to provide a mechanism for name encapsulation to allow same method to be applied to different objects while C89 does not support function overloading. Finally we must provide a mechanism to allow preprocessor macros to accept a variable number of arguments like the C99 __VA_ARGS__ predefined macro does. In the following, we will use some terminology borrowed  from C++ (and Objective C) but with a slight difference about the meaning of object (C++: class object) and class (C++: class). To avoid any confusion, this paper will use the following therminology: To use the proposed OOPC techniques, you need to include the header files ooc.h (depends on exception.h and ooexception.h) in your project and to link it with the ooc.c and exception.c files. If you want to debug your OOPC program, you need to include the header file oodebug.h and to link your project with oodebug.c. To be informed about the OOPC evolution and discussion you can subscribe to the forum-oopc mailing list (see Mailing List).
 
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 Model Representation
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 ctor() has always the same name as the class and therefore it is always called by class.class() (C++: default constructor class::class()). The dtor() has always the same name as the class name preceded by an underscore (C++: tilde) and therefore is always called by class._class() (C++: default destructor class::~class()). The ator() is always called alloc() (C++: default allocator new(class)) and may throw ooc_bad_alloc exception upon failure. In OOPC, the default constructor  and allocator are defined, declared and generated since they return only a well formed instance of the object without any dynamic initialization (only static initializations can be performed), but the default destructor is only defined and declared (not generated) since it may require some dymanic freeing and therefore needs to be provided by the class designer (even if empty).

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:
 

Object Model: Single inheritance
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:
 

Object Model: Multiple inheritance
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 {
  char *name;
};
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:
typedef union {                               /* generated */
  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 */
From this definition, which is exactly what OOPC does, we can do the following remarks: The object designer should never take care about the encapsulation of its data whose purpose is only to reflect the Object Model structure and guarantee the data alignment invariance through inheritance, and instead he should use the OOPC object interface (see person.h for a complete interface):
#define OBJECT person

BASEOBJECT_INTERFACE

  char const* private(name);  /* object member */

BASEOBJECT_METHODS

  void constMethod(print);    /* object method */

ENDOF_INTERFACE

The 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.

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++:

Of course, the variables need to be properly initialized before being used. Usually, the third declaration is used since most of the OOPC macros wait for an object pointer and declaring the pointer as constant protects it against a misassignment.
 
Class

Classes collect and encapsulate the following information:

OOPC macros achieve simply the class definition (and declaration) which in reality looks nearly like:
struct _ooc_class_person {                           /* generated */
  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 */

by the equivalent (which must follow the object interface, see person.h for a complete interface):
CLASS_INTERFACE

  t_person *const classMethod_(new) char const name[] __;
  void method_(init) char const name[] __;

ENDOF_INTERFACE

Here, 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).

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");
sendMsg(per, print);
delete(per);
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:
t_person per[1] = { person.person() };
person.init(per, "Brown");
sendMsg(per, print);
person._person(per);
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.

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();
person.init(&per, "Brown");
sendMsg(&per, print);
person._person(&per);
Dealing with array of automatic objects may be a little more complex:
int i;
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);
}

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.
 
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 complex

BASEOBJECT_INTERFACE

  double public(real);
  double public(imag);

BASEOBJECT_METHODS

  /* no virtual function */

ENDOF_INTERFACE

Public 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:
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_H
#define PERSON_H

#include <ooc.h>

#undef  OBJECT
#define OBJECT person

BASEOBJECT_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

This 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.

Once the person interface is properly defined and included into your file, the following things are available:

In addition, the object designer must provide the already declared but not defined destructor: Finally, it is very common to find the init (or variants) method: Finally, it is common to find (but not always, see Abstract Class) the object factory new (or variants) which most of the time simply calls the allocator alloc() and the initializator init() with the new allocated object. That is why new() usually waits for the same arguments as init().

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>

#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
 

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.

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_H
#define EMPLOYEE_H

#include <person.h>

#undef  OBJECT
#define OBJECT employee

OBJECT_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 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.

The inheritance declarations in implementation are as simple as for the interface (employee.c):

#include <stdio.h>

#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 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.

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_H
#define ACLASS_H

#include <class1.h>
#include <class2.h>

#undef  OBJECT
#define OBJECT aClass

OBJECT_INTERFACE

  INHERIT_MEMBERS_OF (class1);
  INHERIT_MEMBERS_OF (class2);
  ...
OBJECT_METHODS

  INHERIT_METHODS_OF (class1);
  INHERIT_METHODS_OF (class2);
  ...
ENDOF_INTERFACE
 

CLASS_INTERFACE
  ...
ENDOF_INTERFACE

#endif

as well as in implementation:
...
OBJECT_IMPLEMENTATION

  SUPERCLASS (class1),
  SUPERCLASS (class2),
  ...
ENDOF_IMPLEMENTATION

initClassDecl()
{
  ...
  initSuper(class1);
  initSuper(class2);
  ...
}
...
CLASS_IMPLEMENTATION
  ...
ENDOF_IMPLEMENTATION

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:
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:

void
methodDecl(the_message)
{
  sendCMsg(super(this,class1),class1,the_message);
  sendCMsg(super(this,class2),class2,the_message);
}
Now, assuming object to be of class aClass, sendMsg(object, the_message); in a program will send automatically the correct message to all your
superclasses. This way of doing respects the polymorphism (if any, see Polymorphism) of your class and of its superclasses.
 
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 --> person
        --> education
where --> 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.

One important thing in these classes is the overload of their print method, like for the manager example:

void
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);
  ...
}

This 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.

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>

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;
}

will gives the output:
name: Brown
name: Smith
      dept: Cars
name: Collins
      dept: Trucks
      dipl: PhD
      level: 2
and 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.

To simplify polymorphism implementation, ooc.h also provides few interesting macros:

We also find the constMethodOvldDecl(method, super) and methodOvldName(method, super) macros which are the sisters of constMethodDecl(method) and methodName(method) used to declare overloading superclass methods.
 
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)

  1. Build and test specialized (i.e. double) objects and classes (i.e. array).
  2. [Interface only] Replace the header top line #ifndef CLASS_H by (i.e. CLASS == ARRAY):

  3. #if define(GENERIC) || !define(G_CLASS_H)
  4. [Interface only] Replace #define OBJECT object by  (i.e. object == array):

  5. #define OBJECT GENERIC(object)
  6. Replace object by OBJECT everywhere (i.e. array by OBJECT). It should also replace t_object by t_OBJECT everywhere (i.e. t_array by t_OBJECT) since it is a substring.
  7. Replace super by GENERIC(super) everywhere (i.e. memBlock by GENERIC(memBlock) and _memBlock by GENERIC_DTOR(memBlock)).  In implementation you can #define MEMBLOCK GENERIC(memBlock) and #define _MEMBLOCK GENERIC_DTOR(memBlock) and use MEMBLOCK and _MEMBLOCK everywhere to make code more readable.
  8. Replace specialized types by generic types gType1, gType2,... (i.e. gType1 == double)
Specialization (user)
  1. Create a specialized interface (i.e. array.h) based on the following model (assuming that g_array.h is the generic interface):

  2. #define gTypePrefix d
    #define gType1 double
    #include <g_array.h>
    #undef gType1
    #undef gTypePrefix
  3. Create a specialized implementation (i.e. array.c) based on the following model (assuming that g_array.c is the generic implementation):

  4. #define gTypePrefix d
    #define gType1 double
    #include <g_array.c>
After having applied these steps (it takes few minutes) to build up specialized version of your generic code, the new specialized objects and classes with the gTypePrefix concatenated in front of their names (i.e. object t_darray and class darray) are ready to use. Have a look to the memBlock and array examples for more details (see Examples). You can also read the documentation Writing generic code in C and C++ for more complex applications.
 
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++:

The best way to understand the use of exceptions is to learn from example:
#include <stdio.h>
#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;
}

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).

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:

Note: Exceptions are always integers but if you defined -DALLOW_PTREXCEPTION, then OOPC ensures that throwing and catching pointers (object instances, classes or objects typeid) as exceptions is safe (like in C++).

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");
protectPtr(emp, ooc_delete);
...
/* an exception can be thrown here, emp will be safely deleted */
...
unprotectPtr(emp);
delete(emp);
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.

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:message
for 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 #) thrown
for 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 10
0      0x804a828  10       ....  ....  test_protection.c(41):div2_
This 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:
Heap corrupted @ beginning of 0x804a848  - test_protection.c(48):div2_
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**
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.

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 @ 0x804b258
  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
while ooc_printClassInfo(stdout, mng) will print out something like:
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

Interface
OBJECT
t_OBJECT
BASEOBJECT_INTERFACE
BASEOBJECT_METHODS
OBJECT_INTERFACE
OBJECT_METHODS
ABSTRACTCLASS_INTERFACE
CLASS_INTERFACE
ENDOF_INTERFACE
INHERIT_MEMBERS_OF()
INHERIT_METHODS_OF()

Encapsulation
public()
private()

Declaration
classMethod()
classMethod_()
method()
method_()
constMethod()
constMethod_()

Messages
sendMsg()
sendMsg_()
sendCMsg()
sendCMsg_()

Miscellaneous
super()
className()
objCopy()
isA()
offsetOf()
typeid()
base_cast()
super_cast()
static_cast()
dynamic_cast()
delete()  (macro)

IMPLEMENTATION

Implementation
BASEOBJECT_IMPLEMENTATION
OBJECT_IMPLEMENTATION
ABSTRACTCLASS_IMPLEMENTATION
CLASS_IMPLEMENTATION
ENDOF_IMPLEMENTATION
SUPERCLASS()

Initialisation
methodName()
methodOvldName()

Declaration
initClassDecl()
dtorDecl()
classMethodDecl()
classMethodDecl_()
methodDecl()
methodDecl_()
methodOvldDecl()
methodOvldDecl_()
constMethodDecl()
constMethodDecl_()
constMethodOvldDecl()
constMethodOvldDecl_()

Miscellaneous
initSuper()
objDefault()
overload()
sub_cast()

+ INTERFACE keywords
 

CLASS

Generated class members
object()  (constructor)
alloc()   (allocator)

Required class members
_object() (destructor)

Global names
object   (class)
t_object (object)
t_object (generic object)

Global functions
ooc_delete() (function)

EXCEPTION

try {}
catch() {}
catch_any {}
endtry
throw()
exception
protectPtr()
unprotectPtr()
 
 

GENERICITY

t_OBJECT   (generic object type)
GENERIC()
GENERIC_DTOR()

Required defines
gTypePrefix
gType1
 


 
Examples

ISO C89

ISO C99 C++ All these example files are included into oopc.tgz or oopc.zip.
 
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:

List of OOPC references
[1] La programmation par objets en langage C, by A. Gourdin, Technique et Documentation 1991
[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 1993

Some 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

List of C++ references
[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