Default constructor in C

Let's talk about the complete engineering solution that was considered best practice in the olden days.

The problem with structs is that everything is public so there is no data hiding.

We can fix that.

You create two header files. One is the "public" header file used by clients of your code. It contains definitions like this:

typedef struct t_ProcessStruct *t_ProcessHandle;

extern t_ProcessHandle NewProcess();
extern void DisposeProcess(t_ProcessHandle handle);

typedef struct t_PermissionsStruct *t_PermissionsHandle;

extern t_PermissionsHandle NewPermissions();
extern void DisposePermissions(t_PermissionsHandle handle);

extern void SetProcessPermissions(t_ProcessHandle proc, t_PermissionsHandle perm);

then you create a private header file that contains definitions like this:

typedef void (*fDisposeFunction)(void *memoryBlock);

typedef struct {
    fDisposeFunction _dispose;
} t_DisposableStruct;

typedef struct {
    t_DisposableStruct_disposer; /* must be first */
    PID _pid;
    /* etc */
} t_ProcessStruct;

typedef struct {
    t_DisposableStruct_disposer; /* must be first */
    PERM_FLAGS _flags;
    /* etc */
} t_PermissionsStruct;

and then in your implementation you can do something like this:

static void DisposeMallocBlock(void *process) { if (process) free(process); }

static void *NewMallocedDisposer(size_t size)
{
    assert(size > sizeof(t_DisposableStruct);
    t_DisposableStruct *disp = (t_DisposableStruct *)malloc(size);
    if (disp) {
       disp->_dispose = DisposeMallocBlock;
    }
    return disp;
}

static void DisposeUsingDisposer(t_DisposableStruct *ds)
{
    assert(ds);
    ds->_dispose(ds);
}

t_ProcessHandle NewProcess()
{
    t_ProcessHandle proc =  (t_ProcessHandle)NewMallocedDisposer(sizeof(t_ProcessStruct));
    if (proc) {
        proc->PID = NextPID(); /* etc */
    }
    return proc;
}

void DisposeProcess(t_ProcessHandle proc)
{
    DisposeUsingDisposer(&(proc->_disposer));
}

What happens is that you make forward declarations for your structs in your public header files. Now your structs are opaque, which means clients can't dick with them. Then, in the full declaration, you include a destructor at the beginning of every struct which you can call generically. You can use the same malloc allocator for everyone the same dispose function and so. You make public set/get functions for the elements you want exposed.

Suddenly, your code is much more sane. You can only get structs from allocators or function that call allocators, which means you can bottleneck initialization. You build in destructors so that the object can be destroyed. And on you go. By the way, a better name than t_DisposableStruct might be t_vTableStruct, because that's what it is. You can now build virtual inheritance by having a vTableStruct which is all function pointers. You can also do things that you can't do in a pure oo language (typically), like changing select elements of the vtable on the fly.

The important point is that there is an engineering pattern for making structs safe and initializable.


C++ is different from C in this case in the respect that it has no "classes". However, C (as many other languages) can still be used for object oriented programming. In this case, your constructor can be a function that initializes a struct. This is the same as constructors (only a different syntax). Another difference is that you have to allocate the object using malloc() (or some variant). In C++ you would simlpy use the 'new' operator.

e.g. C++ code:

class A {
  public:
    A() { a = 0; }
    int a;
};

int main() 
{
  A b;
  A *c = new A;
  return 0;
}

equivalent C code:

struct A {
  int a;
};

void init_A_types(struct A* t)
{
   t->a = 0;
}

int main()
{
   struct A b;
   struct A *c = malloc(sizeof(struct A));
   init_A_types(&b);
   init_A_types(c);
   return 0;
}

the function 'init_A_types' functions as a constructor would in C++.


You can create initializer functions that take a pointer to a structure. This was common practice.

Also functions that create a struct and initialize it (like a factory) - so there is never a time where the struct is "uninitialized" in the "client" code. Of course - that assumes people follow the convention and use the "constructor"/factory...

horrible pseudo code with NO error checking on malloc or free

somestruct* somestruct_factory(/* per haps some initializer agrs? */)
{
  malloc some stuff
  fill in some stuff
  return pointer to malloced stuff
}


void somestruct_destructor(somestruct*)
{
  do cleanup stuff and also free pointer
  free(somestruct);
}

Someone will probably come along and explain how some early C++ preprocessors/compilers worked to do this all in C.

Tags:

C

Constructor