Object Oriented Programming in C





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:

The procedural model using C functions...

using C functions... The abstract data type model using public interface and private implementation as well as data and names encapsulation.

using public interface and private implementation as well as data and names encapsulation. The object-oriented model using (multiple) inheritance and polymorphism which allows to manipulate different object types through a common interface.

Genericity (C++: templates).

Exceptions (C++: exceptions, try , catch , throw , auto_ptr ).

, , , ). Debugging (dynamic allocation, function call, object construction).

Automatic object construction and destruction (must be explicitly called).

Automatic array of objects construction and destruction (must be done by user).

Non-polymorphic objects (all objects are polymorphic).

Virtual inheritance (too complex and not essential).

Pointer to virtual member function handling polymorphism (too complex or slow).

And more...

void*

__VA_ARGS__

A method is a member function (C++: member function).

is a member function (C++: member function). An object method (message) is a polymorphic member function (C++: virtual member function = method).

(message) is a polymorphic member function (C++: virtual member function = method). A class method is a member function not working on an object (C++: static member function).

is a member function not working on an object (C++: static member function). An object includes all the non-static data plus a hidden pointer (C++: class object with methods).

includes all the non-static data plus a hidden pointer (C++: class object with methods). A virtual table includes all the object methods (C++: virtual table).

includes all the object methods (C++: virtual table). A class includes all the static data, methods and class methods (C++: class).

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:

The object collects the class non-static data (any instances).

collects the class non-static data (any instances). The class collects the class static data, class methods and methods (one instance, doesn't exist in C++).

collects the class static data, class methods and methods (one instance, doesn't exist in C++). The virtual table collects the class object methods (one instance).

collects the class object methods (one instance). The type info collects the class RTTI (one intance).

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:

Methods in the vtbl are object methods (C++: virtual member functions).

are (C++: virtual member functions). Methods in the class working on an object are methods (C++: member functions).

working on an object are (C++: member functions). Methods in the class not working on an object are class methods (C++: static member functions).

ctor()

class.class()

class::class()

dtor()

class._class()

class::~class()

ator()

alloc()

new(class)

ooc_bad_alloc

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;

};

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 */

All objects type have a t_ concatenated in front of its (class) name to specify that is an object/a type (i.e. t_person ).

have a concatenated in front of its (class) name to specify that is an object/a type (i.e. ). All objects begin with a hidden member constant pointer __vptr to their virtual table (i.e. hidden type struct _ooc_vtbl_person ).

begin with a hidden member constant pointer to their (i.e. hidden type ). All objects begin with a hidden member constant pointer __iptr to their virtual table information part (i.e. hidden type struct _ooc_vtbl_object ).

begin with a hidden member constant pointer to their part (i.e. hidden type ). All objects always inherit from the base object t_object (i.e. t_object const private(_) ) or from another object.

always inherit from the base object (i.e. ) or from another object. All objects have their data encapsulated into the m structure through which you can access objects members (i.e. obj->m.name ).

have their data encapsulated into the structure through which you can access objects members (i.e. ). All objects instances have a size equal to sizeof m = sizeof __vptr + sizeof data + data alignment.

#define OBJECT person BASEOBJECT_INTERFACE char const* private(name); /* object member */ BASEOBJECT_METHODS void constMethod(print); /* object method */ ENDOF_INTERFACE

private()

name

public()

BASEOBJECT_METHODS

print

constMethod()

this

method()

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

t_person obj_auto;

t_person *obj_ptr;

t_person *const obj_const_ptr;

t_person const*const const_obj_const_ptr;

Class

Classes collect and encapsulate the following information:

Static data : data with one instance per class (C++: static member).

: data with one instance per class (C++: static member). Class methods : non-polymorphic methods without the this pointer (C++: static member function).

: non-polymorphic methods without the pointer (C++: static member function). Methods : non-polymorphic methods with the this pointer (C++: member function).

: non-polymorphic methods with the pointer (C++: member function). A hidden member constant pointer __vptr to its virtual table.

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 */

CLASS_INTERFACE t_person *const classMethod_(new) char const name[] __;

void method_(init) char const name[] __; ENDOF_INTERFACE

person

t_person

struct _ooc_class_person

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

per

"Brown"

new

ooc_bad_alloc

per

print

per

delete()

delete()

ooc_delete()

t_person per[1] = { person.person() };

person.init(per, "Brown");

sendMsg(per, print);

person._person(per);

per

person()

new

per

init

per

per

per

new

init

delete

_object

init()

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

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

}

memcpy()

__vptr

-DALLOW_OBJCOPY

per[i] = per_ref;

objCopy(obj1, obj2)

obj1=obj2

-DALLOW_OBJCOPY

memcpy

memcpy(&obj1, &obj2, sizeof(obj1))

initArr(size, args)

clearArr(size)

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

sendCMsg(per, person, print);

methodAddr(per, print)(per);

methodAddr()

per

sendMsg()

sendCMsg()

methodAddr(&person, print)(per);

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

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

ooc.h

BASE

OBJECT_INTERFACE

BASE

OBJECT_METHODS

ABSTRACT

CLASS_INTERFACE

ENDOF_INTERFACE

XXX_INTERFACE

OBJECT

person

t_OBJECT

t_person

print

print

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

The type t_person . This is the object type .

. This is the . The class person . This is the object class itself (instance).

. This is the itself (instance). The class method person() . This is the object constructor . A typical use is t_person per = person.person();

. This is the object . A typical use is The class method alloc() . This is the object allocator . A typical use is t_person *const per = person.alloc(); May throw ooc_bad_alloc exception.

. This is the object . A typical use is May throw exception. The generic type t_OBJECT . It is equivalent to t_person except for genericity. Typically used in generic object interface and implementation.

The method _person() . This is the object destructor. A typical use is person._person(per);

init

The method init() . This method does non-constant object initialization, including dynamic initializations like arrays allocation or string duplication. A typical use is person.init(per, "Brown");

new

alloc()

init()

new()

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

", 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



IMPLEMENTATION

person.h

after

IMPLEMENTATION

name

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

INHERIT_MEMBERS_OF()

INHERIT_METHODS_OF()

OBJECT_INTERFACE

OBJECT_METHODS

BASE

employee

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

", 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

SUPERCLASS

person

employee

initClassDecl()

once

person

employee

init()

_employee()

init()

_person()

print

vtbl

print

employee

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 .

sub_cast()

this

t_person

t_employee

sub_cast(object, superclass)

m

sub_cast(object, super)->m.public_member .

sub_cast()

OBJECT

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

...

OBJECT_IMPLEMENTATION SUPERCLASS (class1),

SUPERCLASS (class2),

...

ENDOF_IMPLEMENTATION initClassDecl()

{

...

initSuper(class1);

initSuper(class2);

...

}

...

CLASS_IMPLEMENTATION

...

ENDOF_IMPLEMENTATION

sendMsg(super(object, superclass), message);

sendMsg(super(object, superclass.m.supersuperclass), 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);

}

object

aClass

sendMsg(object, 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 --> person

--> education

-->

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

", sub_cast(this,employee.m.person)->m.level);

} ... initClassDecl()

{

initSuper(employee);

initSuper(education);

overload(employee.person.print) = methodOvldName(print, person);

...

}

employe.person.print

print

print

print

employee

education

sendCMsg()

manager

sub_cast()

super_cast()

static_cast(this, employee.m.person, manager)

super_cast()

dynamic_cast(this, education)

dynamic_cast

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;

}

name: Brown

name: Smith

dept: Cars

name: Collins

dept: Trucks

dipl: PhD

level: 2

print_employee()

print

print_person

sendMsg()

__vptr

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

className( object ) returns a constant string which contains the name of the object's class.

returns a constant string which contains the name of the object's class. isA( object , class ) returns a non-zero value if object is of class class .

returns a non-zero value if is of class . offsetOf( object ) returns the size_t offset of object inside its subobject (if any).

returns the offset of inside its subobject (if any). typeid( object ) returns a pointer to the object RTTI.

returns a pointer to the RTTI. methodAddr( object , message ) returns a function pointer to the object message .

returns a function pointer to the . super_cast( object , superclass ) upcast object to superclass (also available as super() ).

to (also available as ). base_cast( object ) downcast object to its bigger subclass. Returns an untyped pointer.

to its bigger subclass. Returns an untyped pointer. static_cast( object , class , subclass ) downcast object from class to subclass (unsafe).

from to (unsafe). dynamic_cast( object , class ) cast object to class or returns NULL (C++: dynamic_cast , safe).

cast to or returns (C++: , safe). sub_cast( object , class ) equivalent to static_cast( object , class , OBJECT) (only available in implementation).

equivalent to (only available in implementation). overload(object, method) returns the virtual table slot of method (only available in implementation).

constMethodOvldDecl(method, super)

methodOvldName(method, super)

constMethodDecl(method)

methodName(method)

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)

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

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

#define OBJECT GENERIC(object) 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. 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. Replace specialized types by generic types gType1, gType2,... (i.e. gType1 == double )

Create a specialized interface (i.e. array.h ) based on the following model (assuming that g_array.h is the generic interface):

#define gTypePrefix d

#define gType1 double

#include <g_array.h>

#undef gType1

#undef gTypePrefix Create a specialized implementation (i.e. array.c ) based on the following model (assuming that g_array.c is the generic implementation):

#define gTypePrefix d

#define gType1 double

#include <g_array.c>

gTypePrefix

t_darray

darray

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

try { statements } tries to execute the block enclosed into the braces.

tries to execute the block enclosed into the braces. catch( exception ) { statements } executes the instructions enclosed into the braces if exception has been thrown during the execution of the previous try block.

executes the instructions enclosed into the braces if has been thrown during the execution of the previous block. catch_any { statements } executes the instructions enclosed into the braces if an uncaught exception has been thrown during the execution of the previous try block.

executes the instructions enclosed into the braces if an uncaught exception has been thrown during the execution of the previous block. endtry closes the previous try .

closes the previous . throw( exception ) throws the exception exception .

throws the exception . exception is the current exception identifier (integer value) available in the catch_any block. Useful to propagate unprocessed exceptions.

is the current exception identifier (integer value) available in the block. Useful to propagate unprocessed exceptions. protectPtr( pointer , pointer_free ) protects dynamically allocated memory against exceptions.

protects dynamically allocated memory against exceptions. unprotectPtr(pointer) unprotects protected pointers against exceptions. Does not free the memory.

#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

", a, b, ln_(div_(a,b)));

}

catch(zero_divide) {

printf("zero division

");

}

catch(domain_error) {

printf("domain error

");

}

catch_any {

printf("unknow exception %d

", exception);

}

endtry; return EXIT_SUCCESS;

}

div_()

ln_()

main()

errno

div_()

ln_()

main()

printf("unknow exception

");

throw(exception);

try

catch()

catch_any

switch()

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 :

ooc_bad_alloc : memory allocation failure (thrown by alloc() ).

: memory allocation failure (thrown by ). ooc_bad_cast : bad dynamic cast (not used).

: bad dynamic cast (not used). ooc_bad_typeid : bad typeid (not used).

: bad typeid (not used). ooc_bad_exception : bad exception (not used).

-DALLOW_PTREXCEPTION

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

just after

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

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

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_

file:line:function

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

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

ooc_printClassInfo(stdout, mng)

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

ooc.[hc] (OOPC files in C89).

(OOPC files in C89). exception.[hc], ooexception.h (Exception files).

(Exception files). oodebug.[hc] (Debugging files).

(Debugging files). example.[hc] test_example.c (OOPC hierachy examples).

(OOPC hierachy examples). person.[hc] employee.[hc] education.[hc] manager.[hc] test_manager.c (OOPC basic examples).

(OOPC basic examples). g_memBlock.[hc] g_array.[hc] memBlock.[hc] array.[hc] test_array.c (OOPC advanced examples).

(OOPC advanced examples). test_exception.c test_protection.c (exceptions tests examples).

(exceptions tests examples). compile.sh compile_tests.sh (examples of gcc commands).

ooc99.h (OOPC file in C99). Rename it to ooc.h to work with others OOPC files (Not examples).

example.[HC] test_example.C (C++ hierachy examples).

(C++ hierachy examples). person.[HC] employee.[HC] education.[HC] manager.[HC] test_manager.C (C++ basic examples).

(C++ basic examples). compile-cpp.sh (example of g++ commands).

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:

Abusive use of global name to simplify object methods call.

Abusive use of untyped pointers to handle polymorphism.

Abusive use of unprotyped functions to handle polymorphism.

Abusive use of structure field mapping to handle polymorphism and inheritance.

Abusive use of macros for classes definitions and inheritances (two macros per class).

Object messages managed by a switch statement.

statement. Multiple inheritance not supported.

Specific preprocessor required to manage (too complex) techniques.

Some techniques are not portable (problem of data alignment) and/or slow.

Difficult to implement genericity on top of these techniques.

[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

[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