1

The natural way to do OOP in C is to do something like this:

#include <stddef.h>
/* stream operations */
struct _src {
  /** open stream. Return true on success */
  bool (*open)(struct _src *src, const char *fspec);
  /** close stream. Return true on success */
  bool (*close)(struct _src *src);
  /** Returns the actual size of buffer read (<=len), or 0 upon error */
  size_t (*read)(struct _src *src, void *buf, size_t len);
  /* hold the actual FILE* in the case of file stream */
  void *data;
};

Sometime I've seen that the operation(s) are in a separate struct, and design in like this:

#include <stddef.h>
struct _src {
  /* vtable */
  const struct _src_ops *ops;
  /* hold the actual FILE* in the case of file stream */
  void *data;
};

/* stream operations */
struct _src_ops {
  /** open stream. Return true on success */
  bool (*open)(struct _src *src, const char *fspec);
  /** close stream. Return true on success */
  bool (*close)(struct _src *src);
  /** Returns the actual size of buffer read (<=len), or 0 upon error */
  size_t (*read)(struct _src *src, void *buf, size_t len);
};

What's the whole point of doing one way or the other ?

malat
  • 12,152
  • 13
  • 89
  • 158

2 Answers2

2

There seems to be two benefits:

  • The const qualifier blocks the user from re-directing the function pointers, intentionally or accidentally. This will make the function pointers themselves read-only.
  • Having a pointer allows the functions to be a separate "singleton" instance, since those functions are the same for every struct object and only need to get allocated once.

However, doing OOP with function pointers is rather crude and mostly just makes sense in cases where you are absolutely sure that you need polymorphism. There is no this pointer so you end up with awkward syntax such as foo.func(&foo, ...). Notably, the possibility to write "object dot member" is a language syntax thing and not related to OO design as such.

Overall it is weird to spent lots of effort into implementing the somewhat rare and specialized OO concept of inheritance/polymorphism, while at the same time ignoring the much more important OO concept of private encapsulation.

In my experience it is better to just use plain functions but implement opaque type and pass an instance to the struct as a parameter. Deal with inheritance when you actually need it.

Lundin
  • 195,001
  • 40
  • 254
  • 396
  • It's an IO interface, I do not know where the JPEG stream will be stored, maybe at a particular offset in a TIFF file. Or maybe a user want to retrieve it using UDP... – malat Aug 31 '20 at 09:09
  • "However, doing OOP with function pointers is rather crude and mostly just makes sense in cases where you are absolutely sure that you need polymorphism." -- indeed that is the point of using a construct like this. If run-time polymorphism was not needed, you could simply have a series of functions taking an 'instance' of the data as argument. Also private encapsulation is being implemented by making the data member a `void*` in this example. The knowledge of the structure of this data could be restricted to the ops implementations. – th33lf Aug 31 '20 at 09:27
2

You might want to think about the two "styles" of vtable in terms of prototype-based inheritance vs. class based inheritance

The first approach is similar to JavaScript prototype-based inheritance. It make it easier to change the behavior of a single instance by modifying the instance vtable. It is not trivial to change the behavior of all the objects of a certain 'class'.

The second approach is similar to traditional C++ (and Java) inheritance. It make it easier to change the behavior of all the objects of a certain class (by modifying the shared vtable), but it make it slightly more complex to change the behavior of a single instance: in this case, a new vtable need to be created (copied from the original vtable), modified, and assigned to the specific instance.

Depending on your application, you can choose the right implementation.

Assuming that there is no need to assign behavior to specific instances, and there is no need to dynamically modify class behavior (after the initial creation), consider the following factors to make a decision:

  • The shared vtable will have slightly high cost per call (due to need to access vtable vs pointer)
  • The shared vtable will result in lower memory utilization (no need for per-instance vtable)
  • The shared vtable will result in faster object creation (no need to initiatize the vtable per instance).

See more in: prototype based vs. class based inheritance

dash-o
  • 13,723
  • 1
  • 10
  • 37