Search code examples
cclassoopdouble-pointer

Why to cast pointer to double pointer in c?


I am reading OOP in C, and there is this header:

#ifndef _NEW_H
#define _NEW_H

#include <stdarg.h>
#include <stddef.h>
#include <assert.h>

void *new (const void *type, ...);
void delete (void *item);

typedef struct
{
    size_t size;
    void *(*ctor)(void *self, va_list *app);
    void *(*dtor)(void *self);
    void *(*clone)(const void *self);
    int (*differ)(const void *self, const void *x);
} class_t;

inline void *new (const void *_class, ...)
{
    const class_t *class = _class;
    void *p = calloc(1, class->size);

    assert(p);
    *(const class_t **)p = class; //why would you cast to double pointer, when you immediately dereference it?

    if (class->ctor)
    {
        va_list args;
        va_start(args, _class);
        p = class->ctor(p, &args);
        va_end(args);
    }
    return p;
}

#endif //_NEW_H

Now I don't understand this expression:

*(const class_t **)p = class;

What does it mean const class_t ** type? it is like an array of arrays? but if I want to have custom class (that is, not only pointer to the struct class_t, but more "public" methods), the overall class is not an array of types class_t. So Why would I cast a void pointer to double pointer and immediately dereference it? How should I understand it?

from the book about that statement: * (const struct Class **) p = class;

p points to the beginning of the new memory area for the object. We force a conversion of p which treats the beginning of the object as a pointer to a struct class_t and set the argument class as the value of this pointer.


Solution

  • The call to calloc() is being used to allocate an array of 1 element of an unspecified "class" type, where the first member of that type is expected to be a class_t* pointer (thus class->size must be at least sizeof(class_t*), but can be higher). calloc() is likely being used instead of malloc() just so that any additional data members represented by class->size will be zero-initialized, otherwise an explicit memset() would be needed.

    The weird cast+dereference is just so that the code can store the input class pointer directly into that 1st class_t* member of that allocated object.

    An array can be accessed using a double-pointer. Dereferencing such a pointer gives you the address of the 1st element in the array. Which in this case happens to also be the same address as the class_t* member.

    In OOP terms, the layout of an object in memory typically starts with a pointer to the object class's vtable, which contains a list of function pointers to the class's "virtual" methods. When a class is "derived" from, descendants "override" virtual methods by simply setting the object's vtable pointer to a new list of function pointers. This concept of OOP doesn't really exist in C, but it is fundamental to C++. In C, it has to be implemented manually, which is what this code is doing.

    Basically, the code is allocating this memory layout for the allocated object:

               ------------    --------------------
    void *p -> | class_t* | -> | size_t size       |
               ------------    --------------------
               | ...      |    | void (*ctor)()   |
               ------------    --------------------
                               | void (*dtor)()   |
                               --------------------
                               | void (*clone)()  |
                               --------------------
                               | void (*differ)() |
                               --------------------
    

    Another way to accomplish the same assignment would be to use a typedef for the "class" type for easier access, eg the original code is equivalent to this:

    typedef struct
    {
        class_t *vtable;
        // other data members, if class->size > sizeof(class_t*) ...
    } class_info_t;
    
    inline void *new (const void *_class, ...)
    {
        const class_t *class = _class;
        class_info_t *p = (class_info_t*) calloc(1, class->size);
    
        assert(p);
        p->vtable = class;
        // other data members are implicitly zeroed by calloc() ...
    
        ...
    }
    

    Without using any typedefs or casting at all, memcpy() can be used to accomplish the same thing, eg:

    inline void *new (const void *_class, ...)
    {
        const class_t *class = _class;
        void *p = calloc(1, class->size);
    
        assert(p);
        memcpy(p, &class, sizeof(class));
    
        ...
    }