Why to cast pointer to double pointer in c?

Viewed 826

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.

2 Answers

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));

    ...
}

What does it mean const class_t ** type?

It is a pointer to a pointer. It expresses that whatever it points to is actually another pointer. And that 2nd points to the concrete object of type, in this case, class_t

[type] ---> [2nd pointer] ---> [class_t object]

Why *(const class_t **)p = class;?

What this weird construction do is to place class where the "class_t object" is. Now, looking at how this new function is supposed to be used it is not that weird.

The new is a generic constructor of any custom type (struct). There is however a requirement on the type which is that the structure must include pointer to the class_t as a first member. This is to enable polymorphism and basically what C++ does under the hood with a pointer to v-table.

Now, if I would be to define a custom type I would do:

struct Foo {
    void *class_t;
    // whatever members I need for my type
};

How to use it?

Now, when the type is defined there is one more thing to do. Note that the new function as an argument takes a pointer inline void *new (const void *_class, ...) that is used as a foundation const class_t *class = _class; of the one that is being create. It would sort of mean that you need to pass something while you create something - so what is the point?

Well, there is the trick to define a const pointer to the type in it's header that can be used to construct objects of this type.

In the foo header:

struct Foo {
    void *class_t;
    // whatever members I need for my type
};

static const struct class_t _Foo = { 
sizeof (struct Foo), 
// and other functions such ad ctor, dtro, etc...
}; 

const void *Foo = &_Foo;

The above defines a custom type together with a matrix to create all of the objects of this type. It is important to note that it is of type class_t and that the size of the Foo is recorded.

Finally, new objects of Foo are created as follows:

void *f = new(Foo);

The Foo is however of type class_t but we want it to be Foo. The first element of the Foo struct is the pointer to class_t and so in order to create this relation the double pointer needs to be used inside of the new

Related