Writing a ‘generic’ struct-print method in C

There are many different ways that you can do this.

Usually, defining a “common” struct for common information that also has a type field.


Option #1:

Here’s a version that uses void * pointers and a switch in the print function:

#include <stdio.h>
#include <stdlib.h>

typedef struct Common {
    int type;
    const char *name;
} Common;

enum {
    TYPE_CAR,
    TYPE_ANIMAL,
};

typedef struct Car {
    Common comm;
    unsigned int cost;
} Car;

typedef struct Animal {
    Common comm;
    unsigned int age;
    unsigned int weight;
} Animal;

Car *
car_new(const char *name,int cost)
{
    Car *car = malloc(sizeof(*car));

    car->comm.name = name;
    car->comm.type = TYPE_CAR;

    car->cost = cost;

    return car;
}

void
car_print(void *obj)
{
    Car *car = obj;

    printf("The cost is: %d\n",car->cost);
}

Animal *
animal_new(const char *name,int age,int weight)
{
    Animal *animal = malloc(sizeof(*animal));

    animal->comm.name = name;
    animal->comm.type = TYPE_ANIMAL;

    animal->age = age;
    animal->weight = weight;

    return animal;
}

void
animal_print(void *obj)
{
    Animal *animal = obj;

    printf("The age is: %d\n",animal->age);
    printf("The weight is: %d\n",animal->weight);
}

void
print_struct(void *obj)
{
    Common *comm = obj;

    printf("The name is: %s\n", comm->name);

    switch (comm->type) {
    case TYPE_ANIMAL:
        animal_print(obj);
        break;
    case TYPE_CAR:
        car_print(obj);
        break;
    }
}

int
main(void)
{
    Animal *animal = animal_new("Dog",10,200);
    Car *car = car_new("Ford",50000);

    print_struct(animal);
    print_struct(car);

    return 0;
};

Option #2:

Passing around a void * pointer isn’t as type safe as it could be.

Here’s a version that uses Common * pointers and a switch in the print function:

#include <stdio.h>
#include <stdlib.h>

typedef struct Common {
    int type;
    const char *name;
} Common;

enum {
    TYPE_CAR,
    TYPE_ANIMAL,
};

typedef struct Car {
    Common comm;
    unsigned int cost;
} Car;

typedef struct Animal {
    Common comm;
    unsigned int age;
    unsigned int weight;
} Animal;

Common *
car_new(const char *name,int cost)
{
    Car *car = malloc(sizeof(*car));

    car->comm.name = name;
    car->comm.type = TYPE_CAR;

    car->cost = cost;

    return (Common *) car;
}

void
car_print(Common *obj)
{
    Car *car = (Car *) obj;

    printf("The cost is: %d\n",car->cost);
}

Common *
animal_new(const char *name,int age,int weight)
{
    Animal *animal = malloc(sizeof(*animal));

    animal->comm.name = name;
    animal->comm.type = TYPE_ANIMAL;

    animal->age = age;
    animal->weight = weight;

    return (Common *) animal;
}

void
animal_print(Common *obj)
{
    Animal *animal = (Animal *) obj;

    printf("The age is: %d\n",animal->age);
    printf("The weight is: %d\n",animal->weight);
}

void
print_struct(Common *comm)
{

    printf("The name is: %s\n", comm->name);

    switch (comm->type) {
    case TYPE_ANIMAL:
        animal_print(comm);
        break;
    case TYPE_CAR:
        car_print(comm);
        break;
    }
}

int
main(void)
{
    Common *animal = animal_new("Dog",10,200);
    Common *car = car_new("Ford",50000);

    print_struct(animal);
    print_struct(car);

    return 0;
};

Option #3:

Here’s a version that uses a virtual function callback table:

#include <stdio.h>
#include <stdlib.h>

typedef struct Vtable Vtable;

typedef struct Common {
    int type;
    Vtable *vtbl;
    const char *name;
} Common;

enum {
    TYPE_CAR,
    TYPE_ANIMAL,
};

typedef struct Car {
    Common comm;
    unsigned int cost;
} Car;
void car_print(Common *obj);

typedef struct Animal {
    Common comm;
    unsigned int age;
    unsigned int weight;
} Animal;
void animal_print(Common *obj);

typedef struct Vtable {
    void (*vtb_print)(Common *comm);
} Vtable;

void
car_print(Common *obj)
{
    Car *car = (Car *) obj;

    printf("The cost is: %d\n",car->cost);
}

Vtable car_vtbl = {
    .vtb_print = car_print
};

Common *
car_new(const char *name,int cost)
{
    Car *car = malloc(sizeof(*car));

    car->comm.name = name;
    car->comm.type = TYPE_CAR;
    car->comm.vtbl = &car_vtbl;

    car->cost = cost;

    return (Common *) car;
}

Vtable animal_vtbl = {
    .vtb_print = animal_print
};

void
animal_print(Common *obj)
{
    Animal *animal = (Animal *) obj;

    printf("The age is: %d\n",animal->age);
    printf("The weight is: %d\n",animal->weight);
}

Common *
animal_new(const char *name,int age,int weight)
{
    Animal *animal = malloc(sizeof(*animal));

    animal->comm.name = name;
    animal->comm.type = TYPE_ANIMAL;
    animal->comm.vtbl = &animal_vtbl;

    animal->age = age;
    animal->weight = weight;

    return (Common *) animal;
}

void
print_struct(Common *comm)
{

    printf("The name is: %s\n", comm->name);

    comm->vtbl->vtb_print(comm);
}

int
main(void)
{
    Common *animal = animal_new("Dog",10,200);
    Common *car = car_new("Ford",50000);

    print_struct(animal);
    print_struct(car);

    return 0;
};

UPDATE:

wow, that’s such a great answer thanks for putting in all the time. All three approaches are a bit over my head for where I’m at now…

You’re welcome. I notice you have a fair bit of python experience. Pointers (et. al.) are a bit of an alien concept that can take some time to master. But, once you do, you’ll wonder how you got along without them.

But …

If there was one approach out of the three that you’d suggest to start with, which would it be?

Well, by a process of elimination …

Because option #1 uses void * pointers, I’d eliminate that because option #2 is similar but has some type safety.

I’d eliminate option #2 because the switch approach requires that the generic/common functions have to “know” about all the possible types (i.e.) we need each common function to have a switch and it has to have a case for each possible type. So, it’s not very extensible or scalable.

That leaves us with option #3.

Note that what we’ve been doing is somewhat similar to what c++ does for inherited classes [albeit with in a somewhat more verbose manner].

What is called Common here would be termed the “base” class. Car and Animal would be “derived” classes of Common.

In such a situation, c++ would [invisibly] place the Vtable pointer as a transparent/hidden first element of the struct. It would handle all the magic with selecting the correct functions.

As a further reason as to why a Vtable is a good idea, it makes it easy to add new functions/functionality to the structs.

For such an amorphous/heterogeneous collection, a good way to organize this is with a doubly linked list.

Then, once we have a list, we often wish to sort it.

So, I’ve created another version that implements a simple doubly linked list struct.

And, I’ve added a simple/crude/slow function to sort the list. To be able to compare different types, I added a Vtable entry to compare list items.

Thus, it’s easy to add new functions. And, we can add new types easily enough.

… Be careful what you wish for–you may actually get it 🙂

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct Vtable Vtable;

typedef struct Common {
    int type;
    Vtable *vtbl;
    struct Common *prev;
    struct Common *next;
    const char *name;
} Common;

typedef struct List {
    Common *head;
    Common *tail;
    int count;
} List;

enum {
    TYPE_CAR,
    TYPE_ANIMAL,
};

typedef struct Car {
    Common comm;
    unsigned int cost;
} Car;
void car_print(const Common *obj);
int car_compare(const Common *lhs,const Common *rhs);

typedef struct Animal {
    Common comm;
    unsigned int age;
    unsigned int weight;
} Animal;
void animal_print(const Common *obj);
int animal_compare(const Common *lhs,const Common *rhs);

typedef struct Vtable {
    void (*vtb_print)(const Common *comm);
    int (*vtb_compare)(const Common *lhs,const Common *rhs);
} Vtable;

void
car_print(const Common *obj)
{
    Car *car = (Car *) obj;

    printf("The cost is: %d\n",car->cost);
}

int
car_compare(const Common *lhsp,const Common *rhsp)
{
    const Car *lhs = (const Car *) lhsp;
    const Car *rhs = (const Car *) rhsp;
    int cmp;

    cmp = lhs->cost - rhs->cost;

    return cmp;
}

Vtable car_vtbl = {
    .vtb_print = car_print,
    .vtb_compare = car_compare
};

Common *
car_new(const char *name,int cost)
{
    Car *car = malloc(sizeof(*car));
    Common *comm = &car->comm;

    comm->name = name;
    comm->type = TYPE_CAR;
    comm->vtbl = &car_vtbl;

    car->cost = cost;

    return comm;
}

Vtable animal_vtbl = {
    .vtb_print = animal_print,
    .vtb_compare = animal_compare
};

void
animal_print(const Common *obj)
{
    const Animal *animal = (const Animal *) obj;

    printf("The age is: %d\n",animal->age);
    printf("The weight is: %d\n",animal->weight);
}

int
animal_compare(const Common *lhsp,const Common *rhsp)
{
    const Animal *lhs = (const Animal *) lhsp;
    const Animal *rhs = (const Animal *) rhsp;
    int cmp;

    do {
        cmp = lhs->age - rhs->age;
        if (cmp)
            break;

        cmp = lhs->weight - rhs->weight;
        if (cmp)
            break;
    } while (0);

    return cmp;
}

Common *
animal_new(const char *name,int age,int weight)
{
    Animal *animal = malloc(sizeof(*animal));
    Common *comm = &animal->comm;

    comm->name = name;
    comm->type = TYPE_ANIMAL;
    comm->vtbl = &animal_vtbl;

    animal->age = age;
    animal->weight = weight;

    return comm;
}

void
common_print(const Common *comm)
{

    printf("The name is: %s\n", comm->name);

    comm->vtbl->vtb_print(comm);
}

int
common_compare(const Common *lhs,const Common *rhs)
{
    int cmp;

    do {
        cmp = lhs->type - rhs->type;
        if (cmp)
            break;

        cmp = strcmp(lhs->name,rhs->name);
        if (cmp)
            break;

        cmp = lhs->vtbl->vtb_compare(lhs,rhs);
        if (cmp)
            break;
    } while (0);

    return cmp;
}

List *
list_new(void)
{
    List *list = calloc(1,sizeof(*list));

    return list;
}

void
list_add(List *list,Common *comm)
{
    Common *tail;

    tail = list->tail;

    comm->prev = tail;
    comm->next = NULL;

    if (tail == NULL)
        list->head = comm;
    else
        tail->next = comm;

    list->tail = comm;
    list->count += 1;
}

void
list_unlink(List *list,Common *comm)
{
    Common *next;
    Common *prev;

    next = comm->next;
    prev = comm->prev;

    if (list->head == comm)
        list->head = next;

    if (list->tail == comm)
        list->tail = prev;

    if (next != NULL)
        next->prev = prev;
    if (prev != NULL)
        prev->next = next;

    list->count -= 1;

    comm->next = NULL;
    comm->prev = NULL;
}

void
list_sort(List *listr)
{
    List list_ = { 0 };
    List *listl = &list_;
    Common *lhs = NULL;
    Common *rhs;
    Common *min;
    int cmp;

    while (1) {
        rhs = listr->head;
        if (rhs == NULL)
            break;

        min = rhs;
        for (rhs = min->next;  rhs != NULL;  rhs = rhs->next) {
            cmp = common_compare(min,rhs);
            if (cmp > 0)
                min = rhs;
        }

        list_unlink(listr,min);
        list_add(listl,min);
    }

    *listr = *listl;
}

void
list_rand(List *listr)
{
    List list_ = { 0 };
    List *listl = &list_;
    Common *del;
    int delidx;
    int curidx;
    int cmp;

    while (listr->count > 0) {
        delidx = rand() % listr->count;

        curidx = 0;
        for (del = listr->head;  del != NULL;  del = del->next, ++curidx) {
            if (curidx == delidx)
                break;
        }

        list_unlink(listr,del);
        list_add(listl,del);
    }

    *listr = *listl;
}

void
sepline(void)
{

    for (int col = 1;  col <= 40;  ++col)
        fputc('-',stdout);
    fputc('\n',stdout);
}

void
list_print(const List *list,const char *reason)
{
    const Common *comm;
    int sep = 0;

    printf("\n");
    sepline();
    printf("%s\n",reason);
    sepline();

    for (comm = list->head;  comm != NULL;  comm = comm->next) {
        if (sep)
            fputc('\n',stdout);
        common_print(comm);
        sep = 1;
    }
}

int
main(void)
{
    List *list;
    Common *animal;
    Common *car;

    list = list_new();

    animal = animal_new("Dog",10,200);
    list_add(list,animal);
    animal = animal_new("Dog",7,67);
    list_add(list,animal);
    animal = animal_new("Dog",10,67);
    list_add(list,animal);

    animal = animal_new("Cat",10,200);
    list_add(list,animal);
    animal = animal_new("Cat",10,133);
    list_add(list,animal);
    animal = animal_new("Cat",9,200);
    list_add(list,animal);

    animal = animal_new("Dog",10,200);

    car = car_new("Ford",50000);
    list_add(list,car);
    car = car_new("Chevy",26240);
    list_add(list,car);
    car = car_new("Tesla",93000);
    list_add(list,car);
    car = car_new("Chevy",19999);
    list_add(list,car);
    car = car_new("Tesla",62999);
    list_add(list,car);

    list_print(list,"Unsorted");

    list_rand(list);
    list_print(list,"Random");

    list_sort(list);
    list_print(list,"Sorted");

    return 0;
}

Here’s the program output:

----------------------------------------
Unsorted
----------------------------------------
The name is: Dog
The age is: 10
The weight is: 200

The name is: Dog
The age is: 7
The weight is: 67

The name is: Dog
The age is: 10
The weight is: 67

The name is: Cat
The age is: 10
The weight is: 200

The name is: Cat
The age is: 10
The weight is: 133

The name is: Cat
The age is: 9
The weight is: 200

The name is: Ford
The cost is: 50000

The name is: Chevy
The cost is: 26240

The name is: Tesla
The cost is: 93000

The name is: Chevy
The cost is: 19999

The name is: Tesla
The cost is: 62999

----------------------------------------
Random
----------------------------------------
The name is: Ford
The cost is: 50000

The name is: Chevy
The cost is: 26240

The name is: Dog
The age is: 10
The weight is: 200

The name is: Cat
The age is: 10
The weight is: 133

The name is: Dog
The age is: 10
The weight is: 67

The name is: Cat
The age is: 10
The weight is: 200

The name is: Cat
The age is: 9
The weight is: 200

The name is: Dog
The age is: 7
The weight is: 67

The name is: Tesla
The cost is: 93000

The name is: Tesla
The cost is: 62999

The name is: Chevy
The cost is: 19999

----------------------------------------
Sorted
----------------------------------------
The name is: Chevy
The cost is: 19999

The name is: Chevy
The cost is: 26240

The name is: Ford
The cost is: 50000

The name is: Tesla
The cost is: 62999

The name is: Tesla
The cost is: 93000

The name is: Cat
The age is: 9
The weight is: 200

The name is: Cat
The age is: 10
The weight is: 133

The name is: Cat
The age is: 10
The weight is: 200

The name is: Dog
The age is: 7
The weight is: 67

The name is: Dog
The age is: 10
The weight is: 67

The name is: Dog
The age is: 10
The weight is: 200

Leave a Comment