Version 0.5
Copyright (C) 2008 by Zack Smith
All rights reserved.
News
For actual downloadable code, click
here.
Introduction
Object-oriented programming languages (OOPLs) became dominant years ago
over older procedural languages like C, Pascal and the like.
With these languages come fundamental problems and
complexities that are not necessarily desired by a software architect.
The case of C++ exemplifies the pitfalls of using OO languages:
its templates and multiple inheritance
and ever-shifting class libraries make software design,
implementation, and debugging more difficult
than it needs to be.
While some C++ code is
well-written, some C++ code is an illegible tangle of derived classes
worse than any "goto" based problem from the distant past.
And yet, nevertheless a typical C++ partisan will defend
such a C++ tangle as being perfectly valid and appropriate.
Due to the problems of common object-oriented languages,
it is sometimes preferable for a programmer to use a
non-OOP language such as C augmented
to use object-oriented practices.
Key techniques of object-oriented C programming
Struct with pointers
A simple technique for implementing an object in C
is to imitate C++, and provide a struct with pointers
to methods inside.
typedef struct obj {
int a;
char *b;
// Class methods
struct obj (*new)();
void (*delete)(struct obj *);
// Object methods
void (*set_a)(int);
void (*set_b)(char *);
} Object;
|
In use:
Object *obj = class_struct->new();
obj->set_a (123);
|
A possible pitfall of this approach
is that methods in the struct might be NULL
due to lack of initialization or
may have been erroneously overwritten, in either case
resulting in a program crash when used.
And if a function pointer is overwritten, there will be no
easy way to ascertain where the pointer
was overwritten.
Struct and message-passer
A better alternative is to use an approach similar to
Objective C's, and pass messages to objects
based on a method name.
typedef struct obj {
int class_id;
int a;
char *b;
} Object;
|
A send_message function is used to send messages to objects
and is implemented to take variable numbers of arguments.
The class initializer is responsible for
loading all of its methods into a hash of functions.
Note that it's necessary to include "stdarg.h".
int send_message(int count, ...)
{
int tmp;
va_list ap;
va_start(ap, count);
Object *this = va_arg(ap, Object*);
char *msg = va_arg(ap, char*);
// Here, look up method in hash and call appropriate function...
va_end(ap);
}
|
In use:
Object *obj;
obj = send_message (class_struct, "new");
send_message (obj, "set_a", 123);
|
Of course, in practice "send_message" would be shortened
to something more useable, e.g. "S".
In Objective C, one simply encloses
the message passing in brackets e.g.
[ object message: parameters ];
|
An advantage to this approach is that
method pointers are less likely to be
overwritten since they are not in the data struct.
This approach can also be extended
to include the hashing of object data, to provide
further protection against program errors.
A further refinement might be to include some non-textual
predefined numeric method names, e.g. implemented using enum or #define,
which index into a table to avoid doing any
string comparison. For instance:
enum {
MESSAGE_NEW=1,
MESSAGE_DESTROY=2,
MESSAGE_DUMP=3
};
Object *obj = send_message (rectangle_class_struct, MESSAGE_NEW);
send_message (obj, "set_width", 456);
|
Container classes
In C++, the template functionlity permits
the creation of containers of objects, for instance
an ordered list of string objects.
However the use of templates can quickly make code
unreadable.
In C, an equivalent functionality can be
implemented by simply having all containers
deal with only one object type,
which is a 32-bit or 64-bit value,
and by providing the container with any
functions that it may need to process that value,
e.g. to compare two of them in order
to sort a list.
The application logic would then have to
deal with the values themselves
to decide if it is a basic type, e.g. int or float,
or an object pointer.
An alternative is to literally pound-define
the type in question e.g. #define SOMETYPE int, just before pound-including
the container class, e.g.
write the container class to contain things of type SOMEOBJECT,
which is defined in another file.
Download
My implementation of the message-passing approach
is a work in progress.
Links
|
|