Introduction
This article discusses how to cleanly write advanced classes in C.
This is a method I came up with that allows you to create advanced classes in C very cleanly and easily. It's very useful for large projects that need a lot of structure, especially when working on embedded systems that don't have a C++ runtime (or if you just have a beef with C++ in general).
Background
I've fiddled around with this a lot in the past, but it was always very amateurish at best. However, I was writing a post in my blog about programming tricks I've learned over the years in C and the page formatting just didn't do it justice. Besides, CodeProject has helped me overcome numerous obstacles in the past and I felt like giving back, you know?
Using the Code
The first thing that needs to be done is to define a few definitions and macros to make the definition of classes easier. The macros use memcpy and malloc / free, so you'll need to include the following headers:
#include <string.h> // for memcpy
#include <malloc.h> // for malloc / free
The first definitions I'll cover are used to define the class types.
#define class typedef struct
#define class_t void *
#define ref_class static struct
#define partial_class struct
The first definition is used to define normal classes. Unfortunately, because of the way struct
s work in C, the name will have to be after the class definition. It's annoying, but oh well.
The second definition defines the class type which is returned by the constructors, destructors, and operators of the classes.
The third definition defines a reference class. Reference classes have 2 purposes. First, whenever a new class is created, it duplicates the reference class to initialize the pointers to the functions. The second is to provide access to protected
members not contained within the class itself in pretty much the same way you'd use a static
member function in a C++ class. The reasoning behind this is that you wouldn't really want to duplicate 20-30 pointers to functions every time you create a class. You'd instead make them protected
(members of the reference class) so they're still accessible since they never change.
The fourth definition defines partial classes which are pretty much only used to define the private
members of a reference class.
The next definitions are the access modifiers.
#define public {
#define private }, {
#define protected },
You're probably wondering what's going on here, but it'll go without explanation later. The only thing that should be said is that there should always be at least a public
and protected
section in all your reference classes, and the order should always be public
, private
, then protected
.
The next macros are used to declare the constructor, destructor, operators, and members of a class.
#define constructor(...) class_t (*_constructor)(class_t, ##__VA_ARGS__)
#define destructor(...) class_t (*_destructor)(class_t, ##__VA_ARGS__)
#define operator(o) class_t (*op_##o)(class_t, ...)
#define member(type, name, ...) type (*name)(class_t, ##__VA_ARGS__)
The first and second macros are used to declare the class has a constructor or destructor, and the parameters are the parameters to be passed to the functions.
The third macro is used to declare an operator and the parameter is the name of the operation such as 'add
', 'sub
', 'mul
', etc. It adds an 'op_
' prefix to the name and declares it as having 1 or more parameters.
The fourth macro is used to declare a member function and the parameters are the function type, name, and parameters the function takes.
The next macros are used to define the constructor, destructor, operators, and members of a class.
#define constructor_set(...)
._constructor = (class_t(*)(class_t, ##__VA_ARGS__))
#define destructor_set(...)
._destructor = (class_t(*)(class_t, ##__VA_ARGS__))
#define operator_set(o)
.op_##o = (class_t(*)(class_t, ...))
#define member_set(type, name, ...)
.name = (type (*)(class_t, ##__VA_ARGS__))
The first and second macros are used to set the constructor and destructor of the class, and the parameters are the parameters of the function. Immediately after the use of this macro should be the name of a function, for example:
constructor_set() MyClass_constructor
This applies to the other 2 macros as well. The third macro is used to define an operator and the parameter is the operation name.
The fourth macro is used to define a member function, and the parameters are the function type, name, and parameters of the function.
The next macros are used to create a new class and optionally call the constructor.
#define new(type, ...)
(type *)((type *)&_##type)->_constructor(memcpy(malloc
(sizeof(type)), &_##type, sizeof(type)), ##__VA_ARGS__)
#define new_(type) (type *)
memcpy(malloc(sizeof(type)), &_##type, sizeof(type))
#define _new(type, name, ...)
(type *)memcpy(&(name), &_##type, sizeof(type));
name._constructor(&name, ##__VA_ARGS__)
#define _new_(type, name)
(type *)memcpy(&(name), &_##type, sizeof(type))
The macros with a '_
' suffix do not call the constructor on creation, regardless of whether or not it exists.
The macros with a '_
' prefix do not allocate member for class with malloc, and instead just memcpy the reference class into an existing one. The parameters for these are class type, class object (not pointer), and parameters passed to the constructor.
For the macros without a '_
' prefix, the parameters are just class type and parameters to pass to the constructor.
The next macros are used to delete a class and optionally call the destructor:
#define delete(name, ...)
free(name->_destructor(name, ##__VA_ARGS__));
#define delete_(name, ...) free(name);
#define _delete(name, ...) name._destructor(&(name), ##__VA_ARGS__);
#define _delete_(name, ...)
The concept is the same as the new macros; '_
' prefixed ones are for objects with no freeing needed, '_
' suffixed ones don't call the destructor.
Now that I've finally covered the usage of all the macros, I'll explain the process of class creation.
- Declare the class
- Declare the functions
- Define the class
- Define the functions
Class creation must always be done in this exact order, otherwise you'll get undefined reference errors. The next snippet shows how to declare a class:
class {
constructor();
destructor();
const char *helloText;
const char *worldText;
member(int, print, char *);
member(int, println, char *);
} MyClass;
You can see that despite the seemingly complex macros, this is actually fairly easy to understand. Next is step 2, declaring the functions.
class_t MyClass_constructor(MyClass *this);
class_t MyClass_destructor(MyClass *this);
class_t MyClass_op_add(int lop, int rop);
void MyClass_print(MyClass *this, char *text);
void MyClass_println(MyClass *this, char *text);
Note that the constructor, destructor, and operators have a class_t
return type. Also, it's important to note that for the sake of this example, the operator function is using integer arguments which may output a compiler warning. Normally, they'd also be class_t
.
Next is step 3, defining the class:
ref_class {
MyClass MyClass;
partial_class {
int izSecret;
} _MyClass;
operator(add);
} _MyClass = {
public
constructor_set() MyClass_constructor,
destructor_set() MyClass_destructor,
.helloText = "Hello ",
.worldText = "World",
member_set(int, print, char *) MyClass_print,
member_set(int, println, char *) MyClass_println,
private
.izSecret = 5,
protected
operator_set(add) MyClass_op_add,
};
Now if this isn't sexy, I don't know what is.
First thing's first, the name of the reference class should be identical to the name of the class, but with a single '_
' prefix, and the very first member should always be the same type as the class. This is because the reference class extends the class with private
and protected
members.
It's optional, but if they do exist, the private
members must be within a partial class, and they must be the second member of the reference class. For the sake of continuity, you can see I named the first member MyClass
to illustrate that part is public
and visible by the MyClass
class and named the second member _MyClass
to illustrate that part is private
and visible only by the _MyClass
reference class.
After declaring the first and (optional) second members, the rest are declared just like you would any normal class members before closing and moving on to the actual definition.
Here you can see the magic of the public
, private
, and protect
definitions at work, being translated to brackets to define the public
and private
classes; and at the same time giving an almost identical appearance to C++ access modifiers.
Aside from the usual definitions for everything done by the macros, you can see I used '. helloText
' and '.worldText
' to define those. It's not required if you define everything in the same order it was declared, but I still highly suggest doing so anyway in case you need to make any changes later on.
Then lastly we do step 4, defining the functions.
class_t MyClass_constructor(MyClass *this) {
printf("constructor was called\n\n");
return this;
}
class_t MyClass_destructor(MyClass *this) {
printf("\ndestructor was called\n");
return this;
}
class_t MyClass_op_add(int lop, int rop) {
printf("%i\n", lop + rop);
return NULL;
}
void MyClass_print(MyClass *this, char *text) {
printf("%s", text);
}
void MyClass_println(MyClass *this, char *text) {
printf("%s\n", text);
}
It's important to note here that the constructor and destructor functions must return 'this
'.
Now, an example usage:
int main(int argc, char *argv[]) {
MyClass _mc, *mc = _new(MyClass, _mc);
mc->print(mc, (char *)mc->helloText);
_delete_(_mc)
mc = new_(MyClass);
mc->println(mc, (char *)mc->worldText); _MyClass.op_add(4, 5);
delete(mc);
return 0;
}
That's all there is to it!
Points of Interest
While I was writing the code, the operator macro originally took an actual operator like 'operator(+)', which was done mathematically by checking a bunch of conditions against the operator and determining which function to call. The problem though is that it required all operators to be defined and did nothing more than slow down processing, so it was tossed.