Why do we use structure of function pointers?

Viewed 216

As they say, your learn coding techniques from others' code. I've been trying to understand couple of free stacks and they all have one thing in common: Structure of function pointers. I've following of questions related to this architecture.

  1. Is there any specific reason behind such an architecture?
  2. Does function call via function pointer help in any optimization?

Example:

void do_Command1(void)
{
  // Do something
}

void do_Command2(void)
{
  // Do something
}

Option 1: Direct execution of above functions

void do_Func(void)
{
  do_Command1();
  do_Command2();
}

Option 2: Indirect execution of above functions via function pointers

// Create structure for function pointers
typedef struct 
{
 void (*pDo_Command1)(void);
 void (*pDo_Command2)(void);
}EXECUTE_FUNC_STRUCT;

// Update structure instance with functions address    
EXECUTE_FUNC_STRUCT ExecFunc = {
 do_Command1,
 do_Command2,
};


void do_Func(void)
{
  EXECUTE_FUNC_STRUCT *pExecFunc; // Create structure pointer
  
  pExecFun = &ExecFunc; // Assign structure instance address to the structure pointer
    
  pExecFun->pDo_Command1(); // Execute command 1 function via structure pointer
  pExecFun->pDo_Command2(); // Execute command 2 function via structure pointer
}

While Option 1 is easy to understand and implement, why do we need to use Option 2?

3 Answers

While Option 1 is easy to understand and implement, why do we need to use Option 2?

Option 1 doesn't allow you to change the behavior without changing the code - it will always execute the same functions in the same order every time the program is executed. Which, sometimes, is the right answer.

Option 2 gives you the flexibility to execute different functions, or to execute do_Command2 before do_Command1, based decisions at runtime (say after reading a configuration file, or based on the result of another operation, etc.).

Real-world example from personal experience - I was working on an application that would read data files generated from Labview-driven instruments and load them into a database. There were four different instruments, and for each instrument there were two types of files, one for calibration and the other containing actual data. The file naming convention was such that I could select the parsing routine based on the file name. Now, I could have written my code such that:

void parse ( const char *fileName )
{
  if ( fileTypeIs( fileName, "GRA" ) && fileExtIs( fileName, "DAT" ) )
    parseGraDat( fileName );
  else if ( fileTypeIs( fileName, "GRA" ) && fileExtIs ( fileName, "CAL" ) )
    parseGraCal( fileName );
  else if ( fileTypeIs( fileName, "SON" ) && fileExtIs ( fileName, "DAT" ) )
    parseSonDat( fileName );
  // etc.
}

and that would have worked just fine. However, at the time, there was a possibility that new instruments would be added later and that there may be additional file types for the instruments. So, I decided that instead of a long if-else chain, I would use a lookup table. That way, if I did have to add new parsing routines, all I had to do was write the new routine and add an entry for it to the lookup table - I didn't have to modify any of the main program logic. The table looked something like this:

struct lut {
  const char *type;
  const char *ext;
  void (*parseFunc)( const char * );
} LUT[] = { {"GRA", "DAT", parseGraDat },
      {"GRA", "CAL", parseGraCal },
      {"SON", "DAT", parseSonDat },
      {"SON", "CAL", parseSonCal },
      // etc.
    };

Then I had a function that would take the file name, search the lookup table, and return the appropriate parsing function (or NULL if the filename wasn't recognized):

void (*parse)(const char *) = findParseFunc( LUT, fileName );
if ( parse )
  parse( fileName );
else
  log( ERROR, "No parsing function for %s", fileName );

Again, there's no reason I couldn't have used the if-else chain, and in retrospect it's probably what I should have done for that particular app1. But it's a really powerful technique for writing code that needs to be flexible and responsive.


  1. I suffer from a tendency towards premature generalization - I'm writing code to solve what I think will be issues five years from now instead of the issue today, and I wind up with code that tends to be more complex than necessary.

Best explained via Example.

Example 1:

Lets say you want to implement a Shape class with a draw() method, then you would need a function pointer in order to do that.

struct Shape {
    void (*draw)(struct Shape*);
};

void draw(struct Shape* s) {
    s->draw(s);
}

void draw_rect(struct Shape *s) {}
void draw_ellipse(struct Shape *s) {}

int main() 
{
    struct Shape rect = { .draw = draw_rect };
    struct Shape ellipse = { .draw = draw_ellipse };

    struct Shape *shapes[] = { &rect, &ellipse };
    
    for (int i=0; i < 2; ++i)
        draw(shapes[i]);
}

Example 2:

FILE *file = fopen(...);
FILE *mem = fmemopen(...); /* POSIX */

Without function pointers, there would be no way to implement a common interface for file and memory streams.

Addendum

Well, there is another way. Based on the Shape example:

enum ShapeId {
    SHAPE_RECT,
    SHAPE_ELLIPSE
};

struct Shape {
    enum ShapeId id;
};

void draw(struct Shape *s)
{
    switch (s->id) {
        case SHAPE_RECT:    draw_rect(s);    break;
        case SHAPE_ELLIPSE: draw_ellipse(s); break;
    }
}

The advantage of the second example could be, that the compiler could inline the functions, then you would have omitted the overhead of a function call.

"Everything in computer science can be solved with one more level of indirection."

The struct-of-function-pointers "pattern", let's call it, permits runtime choices. SQLite uses it all over the place, for example, for portability. If you provide a "file system" meeting its required semantics, then you can run SQLite on it, with Posix nowhere in sight.

GnuCOBOL uses the same idea for indexed files. Cobol defines ISAM semantics, whereby a program can read a record from a file by specifying a key. The underlying name-value store can be provided by several (configurable) libraries, which all provide the same functionality, but use different names for their "read a record" function. By wrapping these up as function pointers, the Cobol runtime support library can use any of those key-value systems, or even more than one at the same time (for different files, of course).

Related