Carsten Farving
Carsten Farving

Reputation: 541

OOP in C, implicitly pass self as parameter

I've been working on an example to learn OOP in C. Currently I've come up with this code which is working, however I'm interested in making the methods implicitly pass self as a parameter.

#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>
//#include "Stopwatch.h"


typedef struct stopwatch_s
{
    unsigned int milliseconds;
    unsigned int seconds;
    unsigned int minutes;
    unsigned int hours;

    bool is_enabled;

    void ( *tick )      ( struct stopwatch_s* );
    void ( *start )     ( struct stopwatch_s* );
    void ( *stop )      ( struct stopwatch_s* );
    void ( *reset )     ( struct stopwatch_s* );
} stopwatch_t;

static void tick (stopwatch_t* _self)
{
    stopwatch_t * self = _self;
    if (self->is_enabled)
    {
        self->milliseconds++;
        if (self->milliseconds >= 1000)
        {
            self->milliseconds = 0;
            self->seconds++;

            if (self->seconds >= 60)
            {
                self->seconds = 0;
                self->minutes++;

                if (self->minutes >= 60)
                {
                    self->minutes = 0;
                    self->hours++;
                }
            }
        }
    }
}

static void start (stopwatch_t* _self)
{
    stopwatch_t * self = _self;
    self->is_enabled = true;
}

static void stop (stopwatch_t* _self)
{
    stopwatch_t * self = _self;
    self->is_enabled = false;
}

static void reset (stopwatch_t* _self)
{
    stopwatch_t * self = _self;
    self->is_enabled = false;
    self->milliseconds = 0;
    self->seconds = 0;
    self->minutes = 0;
    self->hours = 0;
}

void * new_stopwatch()
{
    stopwatch_t * newInstance = (stopwatch_t *)calloc(1, sizeof(stopwatch_t));
    newInstance->is_enabled = false;
    newInstance->milliseconds = 0;
    newInstance->seconds = 0;
    newInstance->minutes = 0;
    newInstance->hours = 0;
    newInstance->tick = &tick;
    newInstance->start = &start;
    newInstance->stop = &stop;
    newInstance->reset = &reset;

    return newInstance;
}

void main()
{
    struct stopwatch_s * Stopwatch = new_stopwatch();
    printf ("Initial: %d\n", Stopwatch->milliseconds);
    Stopwatch->start (Stopwatch);
    Stopwatch->tick (Stopwatch);
    Stopwatch->tick (Stopwatch);
    Stopwatch->tick (Stopwatch);
    printf ("Started: %d\n", Stopwatch->milliseconds);
    Stopwatch->stop (Stopwatch);
    Stopwatch->tick (Stopwatch);
    printf ("Stopped: %d\n", Stopwatch->milliseconds);
    Stopwatch->reset (Stopwatch);
    printf ("Reset: %d\n", Stopwatch->milliseconds);    
}

I've tried reading and following Object Oriented Programming with ANSI-C, but can't wrap my head around how to structure my "object" so instead of

Stopwatch->tick(Stopwatch);

I can write

Stopwatch->tick();

Upvotes: 19

Views: 11145

Answers (4)

I can't wrap my head around how to structure my "object" so instead of

Stopwatch->tick(Stopwatch);

I can write Stopwatch->tick();

This is not possible in standard C. You need to pass the receiver as an explicit formal argument to your C functions (in contrast with C++ which has this as an implicit formal).

However:

  • you generally want to pack all the method functions in one single struct with several function members (and have each instance start with a pointer to that struct). Read about vtable-s.

  • you could have some macro (or perhaps inline function) to avoid giving Stopwatch twice; you'll still write TICK(Stopwatch) not Stopwatch->tick();; the statement-expr extension of GCC could be useful.

Look into GTK and its Gobject system as an example of a cute object system for C. Read also about the ObjVLisp model and wikipage on virtual method tables. Maybe see this draft report and RefPerSys and also the blog of the late J.Pitrat.

BTW, you could decide that you have first class method selectors (perhaps as integers, or pointers to some common selector type) and code a variadic send dispatching function (so you would code send(StopWatch,TICK_SEL) instead of your dreamed Stopwatch->tick()) or macro. You might find libffi useful. The old Xview could be inspirational.

At last, as many fancy object layer implementors, you might use some metaprogramming and provide some C code generating tool (like moc in Qt). You might even consider customizing your GCC compiler with MELT for such purposes. Or making a translator (see this) from your fancy OOP dialect to C (like VALA or SWIG or Bigloo or Chicken-Scheme do; see also this). Or preprocess your code with an external preprocessor (your own one, or m4 or GPP, etc...).

Upvotes: 17

Matthieu M.
Matthieu M.

Reputation: 300059

Note: there are already a number of good answers, which explain why the "method call" syntax is not available in C, however they do not explain what to do instead but just point at resources. Basic OO in C is actually relatively simple, so here is a quick HOW TO.

This HOW TO is separated in two sections:

  • the first section shows how to achieve encapsulation
  • the second section shows how to layer late binding on top

Encapsulation

Often times, OO is actually used to mean encapsulation. The idea of encapsulation is to obtain a modular design with well defined interfaces over the state of the program in the hope of making it easier to maintain invariants.

In C, this is traditionally achieved through opaque pointers:

// stop_watch.h
typedef struct stop_swatch_ stop_watch;

stop_watch* stop_watch_create();
stop_watch* stop_watch_clone(stop_watch const* sw);
void stop_watch_dispose(stop_watch* sw);

void stop_watch_tick(stop_watch* sw);
void stop_watch_start(stop_watch* sw);
void stop_watch_stop(stop_watch* sw);
void stop_watch_reset(stop_watch* sw);

This header is the only thing the user sees, and therefore it cannot name the internals of the struct stop_watch_. Of course, this being C, the user can still mess with them, but at least we made it a bit harder on them.

Note: the .c is left as an exercise to the reader; it's plain boring C code after all.

Late Binding

Late Binding is deciding at run-time which function to call; it can for example be achieved through virtual methods in C++, Java, ...

It can be done in C, with relative ease, as well. You will just not benefit from all the sugar.

// stop_watch.h
typedef struct stop_watch_functions_ stop_watch_functions;

typedef struct {
    stop_watch_functions const* functions;
} stop_watch;

struct stop_watch_functions_ {
    void (*clone)(stop_watch const*);
    void (*dispose)(stop_watch*);

    void (*tick)(stop_watch*);
    void (*start)(stop_watch*);
    void (*stop)(stop_watch*);
    void (*reset)(stop_watch*);
};

stop_watch* stop_watch_clone(stop_watch const* sw);
void stop_watch_dispose(stop_watch* sw);

void stop_watch_tick(stop_watch* sw);
void stop_watch_start(stop_watch* sw);
void stop_watch_stop(stop_watch* sw);
void stop_watch_reset(stop_watch* sw);

Alright, so we define:

  • a v-table: stop_watch_functions
  • a struct to hold onto that v-table: stop_watch; it is meant to be part of the instance of the concrete stop-watch.

Let's move on to the implementation:

// stop_watch.c
stop_watch* stop_watch_clone(stop_watch const* sw) {
    return (*sw->functions->clone)(sw);
}

void stop_watch_dispose(stop_watch* sw) {
    return (*sw->functions->dispose)(sw);
}

void stop_watch_tick(stop_watch* sw) {
    return (*sw->functions->tick)(sw);
}

void stop_watch_start(stop_watch* sw) {
    return (*sw->functions->start)(sw);
}

void stop_watch_stop(stop_watch* sw)  {
    return (*sw->functions->stop)(sw);
}

void stop_watch_reset(stop_watch* sw) {
    return (*sw->functions->reset)(sw);
}

Pretty straightforward, right?

And finally, let's move on to a concrete stop-watch implementation:

// my_stop_watch.h
#include "stop_watch.h"

typedef struct my_stop_watch_ my_stop_watch;

my_stop_watch* my_stop_watch_create();

stop_watch* my_stop_watch_upcast(my_stop_watch* msw);
my_stop_watch* my_stop_watch_downcast(stop_watch* sw);

Okay, the header is boring; all the good stuff in hidden away after all:

// my_stop_watch.c
#include "my_stop_watch.h"

struct my_stop_watch_ {
    stop_watch base;

    unsigned int milliseconds;
    unsigned int seconds;
    unsigned int minutes;
    unsigned int hours;

    bool is_enabled;
};

static stop_watch* my_stop_watch_clone(stop_watch const* sw) {
    my_stop_watch* new = malloc(sizeof(my_stop_watch));
    memset(new, (my_stop_watch const*)sw, sizeof(my_stop_watch));
}

static void my_stop_watch_dispose(stop_watch* sw) {
    free(sw);
}

static void my_stop_watch_tick(stop_watch* sw) {
    my_stop_watch* msw = (my_stop_watch*)sw;
    /* do something */
}

static void my_stop_watch_start(stop_watch* sw) {
    my_stop_watch* msw = (my_stop_watch*)sw;
    /* do something */
}

static void my_stop_watch_stop(stop_watch* sw) {
    my_stop_watch* msw = (my_stop_watch*)sw;
    /* do something */
}

static void my_stop_watch_reset(stop_watch* sw) {
    my_stop_watch* msw = (my_stop_watch*)sw;
    /* do something */
}

static stop_watch_functions const my_stop_watch_table = {
    &my_stop_watch_clone,
    &my_stop_watch_dispose,

    &my_stop_watch_tick,
    &my_stop_watch_start,
    &my_stop_watch_stop,
    &my_stop_watch_reset
};

my_stop_watch* my_stop_watch_create() {
    my_stop_watch* msw = malloc(sizeof(my_stop_watch*));

    msw->base = &my_stop_watch_table;

    /* do something */

    return msw;
}

stop_watch* my_stop_watch_upcast(my_stop_watch* msw) {
    return &msw->base;
}

my_stop_watch* my_stop_watch_downcast(stop_watch* sw) {
    if (sw->functions != &my_stop_watch_table) {
        return NULL;
    }

    return (my_stop_watch*)((char*)sw - offsetof(my_stop_watch, base));
}

Here I used the strategy of most C++ implementations (with a virtual table); there are other strategies available, but this one is widely applicable.

Upvotes: 13

Lundin
Lundin

Reputation: 214465

Why I never liked that book much, is that tries to turn C into C++. Everyone must first realize that C++ programming is not necessarily the same as object-oriented programming. OOP is a way to do program design and it is quite unrelated to language syntax. C++ just makes it easier and prettier, is all. But just because C++ has a feature that makes code prettier in some situation, it doesn't necessarily mean that the feature is at all related to OOP (take for example operator overloading).

So don't try to turn C into C++. Accept that C has different syntax, which may not be as pretty. C actually has lots of functionality available that lets you implement OOP design. True encapsulation with private/public variables or functions is 100% achievable in C.

Since C is not C++, you don't want member functions inside the struct. The only function pointer you'll want there are special cases such as callback functions and similar. So instead of Stopwatch->tick(&Stopwatch), it is better not to use function pointers at all, but to call the member function directly: sw_tick(&Stopwatch). Where sw is some unique prefix for the stopwatch module.

This allows you to implement Stopwatch as an object of incomplete type (also called "opaque type"), which is the very core of OOP in C. Incomplete type allows you to hide the contents of the struct to the caller.

Then rewrite the whole stopwatch "class" (call it class or ADT or whatever) as something like this:

stopwatch.h

typedef struct stopwatch_t stopwatch_t; // incomplete type

stopwatch_t* sw_new (void);             // "constructor"

void sw_delete (stopwatch_t* sw);       // "destructor"

void sw_tick (const stopwatch_t* sw);   // public member function
// any number of public functions here
// mind const correctness!

stopwatch.c

struct stopwatch_t        // implementation
{
  // true private variables:

  unsigned int milliseconds;
  unsigned int seconds;
  unsigned int minutes;
  unsigned int hours;
  bool is_enabled;
};

stopwatch_t* sw_new (void)
{
  // same as what you already have
}

// the module is responsible for cleaning up its own mess, NOT THE CALLER
void sw_delete (stopwatch_t* sw)
{
  free(sw);
}

// any number of public member functions:
void sw_tick (const stopwatch_t* sw)
{
  // here sw is the "self"/"this" pointer
}

// any number of private member functions:
static void sw_do_stuff (stopwatch_t* sw)
{
}

The caller will only be able to declare pointers to objects, but never an instance of them. That's no big deal, lots of C and C++ libraries work that way. A pointer to incomplete type is somewhat similar to a pointer to an abstract base class in C++. You can't declare instances of those either.

If you need to mix private and public member variables, you would typedef a struct in the h file where the public member variables are declared as plain struct members, and the private member variables are declared through an incomplete type.

Upvotes: 11

Marcus M&#252;ller
Marcus M&#252;ller

Reputation: 36442

That's simpy not a language feature of C, and that probably was one of the motivations to invent C++, so with C alone, this is impossible.

There's a lot of libraries in C, and basically all I know use the same approach as you, using structs to store states (in your case, stopwatch_t), but just skip the function pointers in the structs wherever possible; this is not because C programmers don't like OOP, but because it's less redundant to do e.g.

 stoplib_tick(Stopwatch);

instead of

 Stopwatch->tick(Stopwatch);

Also, it's a bad idea to carry around a bunch of function pointers in every instance of your struct that are always the same; it's just a waste of space, and a possible cause for error. If you want, you can make one struct that contains all the function pointers for your type, and call them from that table. Basically, that's what VTables in C++ are, underneath.

Hence, no C programmer would do this; unless your function pointer is actually something that might change, you just don't keep the operations you can do on a struct in that struct.

I don't know that book you're referring to, but if it preaches doing that, I don't like it.

Seriously, if you want C with object orientation, go for C++; aside from coding for a few kernels, there's very little you can't do with C++ that you can do with C, and it was really invented half an eternity ago to bring object orientation to C programmers. Don't be the 1960s.

EDIT started to read the PDF you link to -- seriously, who would use ANSI-C nowadays? Especially if you want to comfortably work with structs etc, you should not use anything older than C99 (considering that's pretty old already...), and hence, that book is hopelessly outdated, unless you come up with a system of huge importance and heritage ("hi, I'm working on nuclear weapon control systems from the 1980s and I need to fix this and that"), I'd say I can't think of a case where it would make sense following these examples; obviously, "I'm learning to do OOP in C from scratch" shouldn't be based on things that have been obsolete for more than a decade.

EDIT: you comment

As where I come from is an embedded environment, sticking to ANSI-C, things will always work.

I kind of reluctantly agree. C99 support is lacking on some platforms, but: most compilers support the vast majority of C99 features. ANSI-C (should more correctly be called C89, because C99 is also an ANSI standard) is really more than 25 years old now, and without knowing, your code might even not comply with C89. The code in the book sure as hell is not valid ANSI-C, no matter what the author claims. ANSI-C doesn't have // comments, for example; this is but a minor mistake, and I'd guess all compilers, unless set to pedantic mode wouldn't complain, but still, it's not a good sight.

So: do yourself a favor and don't rely on a book that's selectively using hard-to-use language statuses and try to use whatever your compiler supports.

Also: the more I read that book, the less a good excercise in (modern) OOP it seems to be (p. 3, on the PDF page 9):

The generic pointer void * is used throughout. On the one hand it makes it impossible to discover what a set looks like, but on the other hand it permits us to pass virtually anything to add() and the other functions.

Yes, because type safety isn't a concept vital to the success of C, and because it's a good idea to pass void* and then, first line of the method, cast it to your desired pointer type. Aargh! If you want terrible bugs, that's how you get them.

Look at things like CPython: Python is a OO language, yet the interpreter/compiler is written in C. Python does C-OOP with the PyObject struct, which, as a main feature, has a type reference, so to avoid doing these blind casts. You shouldn't be able to pass a const char[] where you're expecting a pointer to one of your objects, and the whole point of polymorphism is that you can use daughter types of your type, but not completely disparate types, with a function. The book really doesn't do OOP any good. Read something else. I'm pretty sure there's books on CPython's design, and I'd personally think that they can't be worse.

Upvotes: 6

Related Questions