Creating "classes" in C, on the stack vs the heap?

Solution 1:

There are several reasons for this.

  1. Using "opaque" pointers
  2. Lack of destructors
  3. Embedded systems (stack overflow problem)
  4. Containers
  5. Inertia
  6. "Laziness"

Let's discuss them briefly.

For opaque pointers, it enables you to do something like:

struct CClass_;
typedef struct CClass_ CClass;
// the rest as in your example

So, the user doesn't see the definition of struct CClass_, insulating her from the changes to it and enabling other interesting stuff, like implementing the class differently for different platforms.

Of course, this prohibits using stack variables of CClass. But, OTOH, one can see that this doesn't prohibit allocating CClass objects statically (from some pool) - returned by CClass_create or maybe another function like CClass_create_static.

Lack of destructors - since C compiler will not automatically destruct your CClass stack objects, you need to do it yourself (manually calling the destructor function). So, the only benefit left is the fact that stack allocation is, in general, faster than heap allocation. OTOH, you don't have to use the heap - you can allocate from a pool, or an arena, or some such thing, and that may be almost as fast as stack allocation, without the potential problems of stack allocation discussed below.

Embedded systems - Stack is not an "infinite" resource, you know. Sure, for most applications on today's "Regular" OSes (POSIX, Windows...), it almost is. But, on embedded systems, stack may be as low as a few KBs. That's extreme, but even "big" embedded systems have stack that are in MBs. So, it will run out if over-used. When it does, mostly there is no guarantee what will happen - AFAIK, in both C and C++ that's "Undefined behaviour". OTOH, CClass_create() can return NULL pointer when you're out of memory, and you can handle that.

Containers - C++ users like stack allocation, but, if you create a std::vector on stack, its contents will be heap allocated. You can tweak that, of course, but that is the default behaviour, and it makes ones life much easier to say "all members of a container are heap-allocated" rather than trying to figure out how to handle if they are not.

Inertia - well, the OO came from SmallTalk. Everything is dynamic there, so, the "natural" translation to C is the "put everything on the heap" way. So, the first examples were like that and they inspired others for many years.

"Laziness" - if you know you only want stack objects, you need something like:

CClass CClass_make();
void CClass_deinit(CClass *me);

But, if you want to allow both stack and heap, you need to add:

CClass *CClass_create();
void CClass_destroy(CClass *me);

This is more work to do for the implementer, but is also confusing to the user. One can make slightly different interfaces, but it doesn't change the fact that you need two sets of functions.

Of course, the "containers" reason is also partially a "laziness" reason.

Solution 2:

Assuming, as in your question, CClass_create and CClass_destroy use malloc/free, then for me doing following is bad practice:

void Myfunc()
{
  CClass* myinstance = CClass_create();
  ...

  CClass_destroy(myinstance);
}

because we could avoid a malloc and a free easily:

void Myfunc()
{
  CClass myinstance;        // no malloc needed here, myinstance is on the stack
  CClass_Initialize(&myinstance);
  ...

  CClass_Uninitialize(&myinstance);
                            // no free needed here because myinstance is on the stack
}

with

CClass* CClass_create()
{
   CClass *self= malloc(sizeof(CClass));
   CClass_Initialize(self);
   return self;
}

void CClass_destroy(CClass *self);
{
   CClass_Uninitialize(self);
   free(self);
}

void CClass_Initialize(CClass *self)
{
   // initialize stuff
   ...
}

void CClass_Uninitialize(CClass *self);
{
   // uninitialize stuff
   ...
}

In C++ we'd also rather do this:

void Myfunc()
{
  CClass myinstance;
  ...

}

than this:

void Myfunc()
{
  CClass* myinstance = new CCLass;
  ...

  delete myinstance;
}

In order to avoid an unnecessary new/delete.

Solution 3:

In C, when some component provides a "create" function, the component implementer is also in control over how the component is initialised. So it not only emulates C++' operator new but also the class constructor.

Giving up on this control over initialisation means a lot more error checking on the inputs, so keeping control makes it easier to provide consistent and predictable behaviour.

I also take exception to malloc always being used to allocate memory. This may often be the case, but not always. For instance, in some embedded systems, you'll find that malloc/free is not used at all. The X_create functions can allocate in other ways, e.g. from an array whose size is fixed at compile-time.