2

First, this is merely an academic question. I know C is not the job for doing OOP programming, but this is more of a learning exercise for a beginner learning what's possible and what's not (or perhaps, what might be possible but is not a good idea).

Let's take the following as a starting place, where I have two different objects but I want to give each of them the same two methods: create and print. I've omitted any error checking, freeing, etc. just to simplify matters:

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

struct Person {
    char* name;
    int age;
};
struct Car {
    char* make;
    char* model;
    int year;
};
struct Person* create_person(void)
{
    struct Person *new = malloc(sizeof (struct Person));
    return new;
}
void print_person(struct Person* person)
{
    printf("<Person: %s (%d)>\n", person->name, person->age);
}
struct Car* create_car(void)
{
    struct Car *new = malloc(sizeof (struct Car));
    return new;
}
void print_car(struct Car* car)
{
    printf("<Car: %s - %s (%d)>\n", car->make, car->model, car->year);
}
int main(void)
{
    struct Car *car = create_car();
    *car = (struct Car) {.make="Chevy", .model="Eldorado", .year=2015};
    print_car(car);

    struct Person *person = create_person();
    *person = (struct Person) {.name="Tom", .age=30};
    print_person(person);
}

I would think that the first part would be to group the 'methods' into the struct itself. So then we would have:

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

struct Person {
    char* name;
    int age;
    void (*print)(struct Person*);
};
struct Car {
    char* make;
    char* model;
    int year;
    void (*print)(struct Car*);
};
void print_car(struct Car* car);
void print_person(struct Person* person);
struct Person* create_person(void)
{
    struct Person *new = malloc(sizeof (struct Person));
    return new;
}
void print_person(struct Person* person)
{
    printf("<Person: %s (%d)>\n", person->name, person->age);
}
struct Car* create_car(void)
{
    struct Car *new = malloc(sizeof (struct Car));
    return new;
}
void print_car(struct Car* car)
{
    printf("<Car: %s - %s (%d)>\n", car->make, car->model, car->year);
}
int main(void)
{
    struct Car *car = create_car();
    *car = (struct Car) {.make="Chevy", .model="Eldorado", .year=2015, .print=print_car};
    car->print(car);

    struct Person *person = create_person();
    *person = (struct Person) {.name="Tom", .age=30, .print=print_person};
    person->print(person);
}

What would be the next step in making it "more OOP like"? Perhaps using preprocessor glue and generics? What would be an example of the most OOP-like that the two objects could become? Again, I know this isn't what C is meant for, but it's more a learning experience.

David542
  • 104,438
  • 178
  • 489
  • 842
  • 2
    David, if you haven't found this little paper yet, it has a creative approach to doing OOP with C. Aptly titled [Object-oriented Programming with ANSI C (Chapter 2)](https://www.cs.rit.edu/~ats/books/ooc.pdf) I note Chapter 2, as that is where the nuts and bolts of the methods used are discussed. There is reasonable inheritance and the other OPP traits. Well worth a read through to see if it may not hold a few approaches you may make use of. The methodology is sound and quite capable, but it is a bit tedious to implement -- which is why OOP in C isn't a big thing... – David C. Rankin Mar 03 '21 at 04:03
  • @DavidC.Rankin I see, thanks for the link. Have you read the interfaces + implementations for C? I hear that book recommended a lot for OOP concepts in C. – David542 Mar 03 '21 at 04:05
  • No, I haven't seen that one yet, but I'll look it up. I worked through all the OOP I could find years ago (5-6 or so). And, while I liked the neat approaches to solving problems, I always came back to the reality that for every new problem, the re-implementation of all the nice approaches, generally took as much time as writing a tailored solution to begin with. Most all of it due to C being strongly typed. Even with the introduction of `_Generic`, seems I always ended up writing another tailored approach to a problem. (you may find the breakthrough I missed `:)` – David C. Rankin Mar 03 '21 at 04:11

1 Answers1

3

You could use approach applied by the Linux kernel. The implementation of OOP is based using a composition for inheritance, and embedding interfaces into new classes.

The macro container_of lets easily alternate between a pointer to class and a pointer to one of its members. Usually an embedded object will be an interface of the class. To find more details about container_of macro see my answer to other but related question.

https://stackoverflow.com/a/66429587/4989451

This methodology was used to create a huge complex object oriented software, i.e. Linux kernel.

In the examples from the question, the Car and Person classes use an printing interface that we can call struct Printable. I strongly suggest to produce a fully initialized objects in create_... functions. Let it make a copy of all strings. Moreover, you should add destroy_... methods to release resources allocated by create_....

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

#define container_of(ptr, type, member) \
  (type*)(void*)((char*)ptr - offsetof(type, member))

struct Printable {
    void (*print)(struct Printable*);
};

struct Person {
    char* name;
    int age;
    struct Printable printable;
};

void print_person(struct Printable *printable) {
    struct Person *p = container_of(printable, struct Person, printable);
    printf("<Person: %s (%d)>\n", p->name, p->age);
}

struct Person *create_person(char *name, int age) {
    struct Person *p = malloc(sizeof *p);
    p->name = strdup(name);
    p->age = age;
    p->printable.print = print_person;
    return p;
}

struct Car {
    char* make;
    char* model;
    int year;
    struct Printable printable;
};

void print_car(struct Printable *printable) {
    struct Car *c = container_of(printable, struct Car, printable);
    printf("<Car: %s - %s (%d)>\n", c->make, c->model, c->year);
}

struct Car *create_car(char *make, char *model, int year) {
    struct Car *c = malloc(sizeof *c);
    c->make = strdup(make);
    c->model = strdup(model);
    c->year = year;
    c->printable.print = print_car;
    return c;
}

void print(struct Printable *printable) {
    printable->print(printable);
}

int main() {
    struct Car *car = create_car("Chevy", "Eldorado", 2015);
    struct Person *person = create_person("Tom", 30);
    print(&car->printable);
    print(&person->printable);
    return 0;
}

produces output:

<Car: Chevy - Eldorado (2015)>
<Person: Tom (30)>

Note that function print() takes a pointer to the Printable interface. The function does not need to know anything about the original class. No preprocessor is used. All casts are done on "library" side, not on clients side. The library initializes the Printable interface, therefore it cannot be misused.

You can easily add other base classes or interfaces like i.e. RefCounted to solve memory management. The i-face would contain a pointer to destructor and refcount itself. Other examples are intrusive linked lists or binary trees.

tstanisl
  • 13,520
  • 2
  • 25
  • 40
  • question, why do you pass `print_person(struct Printable *printable)` instead of just `print_person(struct Person *person)` if the function is entirely customized to the `person` type? – David542 Mar 03 '21 at 22:09
  • also, one more thing: why do use `c->make = srdup(make)` instead of just `c->make = make` ? Wouldn't the pointer point to the existing string literal, or what's the advantage of duplicating the string? – David542 Mar 03 '21 at 22:10
  • 1
    @David542, about `strdup()`. It would work with literals. But it would not work if the the name was not a literal, i.e. loaded from a file. You would have to allocate new name each time. What if some other `Person` object used a literal. The only scalable solution is `name = strdup()` in `create_person()` and `free(name)` in `destroy_name()`. – tstanisl Mar 03 '21 at 22:56
  • @David542, because `print_person` is dedicated to be used by Printable interface. It may be good idea to replace `print_person` with `print_person_ops`. The rest of the world would use `print(&person.printable)` that could be wrapped as `print_person(&person)` – tstanisl Mar 03 '21 at 23:03
  • got it, thank you. So if I were to do a 'shared free' function the function might look something like: `void free_(Freeable* freeable) { freeable->free_(freeable); }` – David542 Mar 03 '21 at 23:05
  • 1
    No. Just use `void destory_person(struct Person *p) { free(p->name); free(p); }`. :) `RefCounted` is worth playing because it greatly simplifies management of lifetime of objects. If would be something similar to `kref` interface in Lunux kernel. See https://www.kernel.org/doc/Documentation/kref.txt – tstanisl Mar 03 '21 at 23:07
  • right, I meant to create a new field within both structs and create a shared function like you did in the `print` method where it routes it to either `print_car` or `print_person`. – David542 Mar 03 '21 at 23:10
  • Ah, the `strdup` uses a malloc, I didn't know that until you pointed it out. Thanks for mentioning that too! – David542 Mar 03 '21 at 23:14