Object-Oriented C Programming -- Part III

Constructors, Destructors, and Allocators (Draft)

This is a follow-up to the old post object-oriented C programming - part II that I've had sitting written but unplublished in my draft queue for years. It's not quite complete, but I figure this article has sat here long enough it deserves publishing.

Allocation and Construction

We've already seen what a C-style object allocator would look like in the mystruct_create method example above. That method served as a replacement for the C++ new operator, which is the standard memory allocator for C++. However, more complex objects have more complex needs. Many objects need constructors to put the object into a valid state. Many objects also need proper destructors to release resources such as memory, handles, and reference counters. Some objects require specialized allocators to efficiently use. Thankfully, all of these things are possible in C.

First, let's consider a basic allocator and constructor. In C++, these are two separate methods. The first is the object's constructor, which is responsible for initializing data. The second is the allocator, which is implemented via the operator new method. There are a few different ways we can implement similar functionality in C, depending on our needs and how much flexibility we want to give the user. I'm going to assume that we're using a fully encapsulated type, with the structure definition hidden away in a private source code file. First, let's consider a simple C function that allocates and initializes an object.

// C++
class MyClass::MyClass() : my_integer(42), my_real(1.23) {}

// C
MyStruct* mystruct_create(void)
{
  MyStruct* self = (MyStruct*)malloc(sizeof(MyStruct));
  self->my_integer = 42;
  self->my_real = 1.23;
  return self;
}

The C version is clearly more verbose. We are require to write the allocation code ourselves as there is no default new operator. Initializing the fields is a little more verbose, and we have to return the instance pointer manually. However, this can be simplified a bit for any structure that we just want to initialize to all zeros; we can use calloc instead of malloc to allocate the structure.

A constructor is helpful, but a destructor is also necessary in the API, even for simple objects that simply need to be freed. Consider for a moment the case where the library is linked against one implementation of malloc/free and the application is linked against another. Even aside from that case, consistency requires that every object has its own destructor/deallocator. Plus, if the object is even extended, having the destructor already in use in client code makes the change that much easier to perform.

// public header
extern void mystruct_free(MyStruct*);

// private implementation
void mystruct_free(MyStruct* self)
{
  free(self);
}

Looking at both code snippets above, the C version is a bit weaker than the C++ version. In particular, what if the user doesn't want to use the standard allocator but instead wants to use their own? The standard way of doing that in C++ is to override the new operator or to use placement new. For container classes, using a template parameter to specify an allocator. With C, the most natural interface is to simple allow the user to pass in an alternative allocator function.

// public header
typedef (void*)(*MyStructAllocator)(size_t);

extern MyStruct* mystruct_create_with_allocator(MyStructAllocator);
extern MyStruct* mystruct_create(void);

// implementation
MyStruct* mystruct_create_with_allocator(MyStructAllocator allocator)
{
  MyStruct* self = (MyStruct*)allocator(sizeof(MyStruct));
  self->my_integer = 42;
  self->my_real = 1.23;
  return self;
}

MyStruct* mystruct_create(void)
{
  return mystruct_create_with_allocator(malloc);
}

This is a bit more work than simply overloading the new operator, but it's also more flexible, allowing run-time selection of the allocator used for each instance of an object. Keeping the default implementation around is a good idea, as most of the time the user will want a plain malloc-based allocation anyway.

Naturally, we are probably going to need an alternative deallocator as well, as most malloc replacements have corresponding free replacements. There are two ways to take care of this. The first is to simply make an alternative version of the mystruct_free function that takes a function pointer. The second is to pass a second function pointer to the mystruct_create_with_allocator, store that function pointer in the object (even for object instances that used the default malloc implementation), and then use that function pointer when mystruct_free is called. The former approach avoids storing the extra function pointer but makes the API a little harder to use and a lot easier to use incorrectly.