Version 0.12
Copyright © 2008-2009 by Zack Smith
All rights reserved, except for array_template.h which is covered by the GPL.
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:
overuse of its templates and multiple inheritance 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.
Approach A: 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)(struct obj*,int);
void (*set_b)(struct obj*,char *);
} Object;
|
In use:
Object *obj = class_struct->new();
obj->set_a (obj, 123);
|
Or a more memory-efficient approac would be to
put the function pointers in a table that's common
to the class, thus:
typedef struct {
// Class methods
struct obj (*new)();
void (*delete)(struct obj *);
// Object methods
void (*set_a)(struct obj*,int);
void (*set_b)(struct obj*,char *);
} ObjectMethods;
typedef struct obj {
int a;
char *b;
ObjectMethods* methods;
} Object;
|
In use:
Object *obj = class_struct->new();
obj->methods->set_a (obj, 123);
|
A possible pitfall of this approach
is that methods in the struct might be NULL
due to lack of proper initialization or
may have been erroneously overwritten.
In either case resulting in a program crash would result
unless a dedicated "method caller" routine were used to checked for NULLs
and unlikely pointer values.
And if a function pointer is overwritten, there will be no easy way
to ascertain where the pointer was overwritten.
Example: FrugalWidgets
I created a small widget set for X Windows that uses this
struct-with-pointers approach. See
here for more information.
Approach B: 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);
|
Templates in C
One of the heavily promoted selling points of C++ has always been templates.
The claim goes that you cannot create templates in C therefore C is bad.
If you can't do templates then you're lost in the wilderness
of having to create a separate List for int, float, char*, bool, et cetera.
Alas, it's an empty claim.
It really goes to show how little imagination some people have.
In reality you can create templates in C without any problem
if you want to.
Approach A: Putting the implementation into a macro
This subsection has been superceded by my page dedicated
to this topic:
Templates in C.
I like this approach for two reasons:
- It is in accord with common C usage.
- It forces you to keep the data type small since it is rather painful to write a long data type.
Here's my example of an Array type:
// array_template.h:
// This code is copyright (C) 2009 by Zack Smith.
// This .h file is covered by the GNU Public License version 2.
#define ARRAY_TEMPLATE(AAA) \
typedef struct { \
int size; \
AAA *objects; \
} Array_##AAA ; \
Array_##AAA *Array_##AAA##_new (int size) { \
Array_##AAA *nu = (Array_##AAA *)malloc(sizeof(Array_##AAA)); \
nu->size = size; \
nu->objects = (AAA *)malloc(size*sizeof(AAA)); \
return nu; \
}; \
void Array_##AAA##_delete (Array_##AAA *ary) { \
if (ary) { \
if (ary->objects) \
free (ary->objects); \
free (ary); \
} \
}; \
AAA Array_##AAA##_get(Array_##AAA *ary, int index) { \
if (!ary) { \
return 0; \
} \
if (index < 0 || index >= ary->size) { \
return 0; \
} \
return ary->objects[index]; \
} \
void Array_##AAA##_set(Array_##AAA *ary, int index, AAA value) { \
if (!ary || !value) { \
return; \
} \
if (index < 0 || index >= ary->size) { \
return; \
} \
ary->objects[index] = value; \
}
|
Notice my use of the C preprocessor's standard ## notation for concatenating
a
#define symbol with other text.
This approach puts the typedef and implementation
in the C file, but in theory one could rewrite
array_template.h
to permit the generation of separate .c and .h files for templated types.
At any rate, here's a simple example of how to use an Array of
int:
#include <stdio.h>
#include "array_template.h"
ARRAY_TEMPLATE(int) // Declare code that we need.
int
main()
{
Array_int *a;
a = Array_int_new (100);
Array_int_set (a, 33, 6);
printf ("a[33] is %d\n", Array_int_get (a, 33));
Array_int_set (a, 33, 5);
printf ("a[33] is now %d\n", Array_int_get (a, 33));
Array_int_delete (a);
}
|
There is one limitation to this approach.
You cannot specify a type that has a "*" or brackets in it.
But you can always typedef that.
For instance:
#include <stdio.h>
#include "array_template.h"
typedef char* string;
ARRAY_TEMPLATE(string) // Declare code that we need.
int
main()
{
Array_string *a;
a = Array_string_new (100);
Array_string_set (a, 33, "this is a test");
printf ("a[33] is '%s'\n", Array_string_get (a, 33));
Array_string_set (a, 33, "2nd test");
printf ("a[33] is now '%s'\n", Array_string_get (a, 33));
Array_string_delete (a);
}
|
The output of this is:
$ ./a
a[33] is 'this is a test'
a[33] is now '2nd test'
Approach B: Inclusion of a .c file
This second approach is not one I like but you see it mentioned in
Usenet forums.
You write a C file for a data type, let's say a list.
But everywhere that data type's name should be you insert MYTYPE
or a similar #define.
Then you do the following, using the example of MYTYPE being "int".
#define MYTYPE int
#include "array_template.c"
List_int *myList;
|
Download
Links
|
|