Writing a 'generic' struct-print method in C

|▌冷眼眸甩不掉的悲伤 提交于 2021-02-07 11:15:16

问题


Is it possible to do something like the following in C, to print a different-but-similar struct type?

#include<stdio.h>

typedef struct Car {
    char*        name;
    unsigned int cost
} Car;

typedef struct Animal {
    char*           name;
    unsigned int    age;
    unsigned int    weight
} Animal;

void print_struct(void *obj) {
    printf("The name is: %s\n", obj->name);
};

int main(void)
{
    Animal *dog = & (Animal) {.name = "Dog", .age = 10, .weight = 200};
    Car *ford   = & (Car) {.name = "Ford", .cost = 50000};

    print_struct(dog);

};

Specifically, the print_struct method:

  • Is this possible to do, and if so how?
  • Is this considered good or bad practice in C to create non-type-specific functions?

Otherwise, wouldn't the code be littered with dozens (maybe even hundreds? in a large project) of functions that look like:

void print_animal(Animal *animal) {
    printf("The name is: %s\n", animal->name);
};
void print_car(Car *car) {
    printf("The name is: %s\n", car->name);
};
...
print_animal(dog);
print_car(ford);

回答1:


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


来源:https://stackoverflow.com/questions/65621027/writing-a-generic-struct-print-method-in-c

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!