Is it a bad idea to create a generic "function pointer" union in C?

Viewed 389

For a project I'm working on, it's desirable to have a generic "pointer to a function" type. However, in C, to have a pointer to a function, you need to specify the prototype in the type of the function pointer.

For example, if I have the function void setdata(short data), I can't store that in the same pointer as I would a function int getdata(), because their arguments and return values are different.

After some lateral thinking, I came up with the following workaround:

typedef long (* PFL)(); /* pointer to function that returns a long... */
typedef short (* PFI)(); /* pointer to function that returns a short... */
typedef void (* PFV)(); /* pointer to function that returns a void... */
typedef void (* PFVAL)(long); /* pointer to function that returns a void...
                                but has a long argument...*/
typedef void (* PFVAI)(short); /* pointer to function that returns a void...
                                but has an short argument...*/
typedef void (* PFVAU)(unsigned short);
typedef void (* PFVAII)(short, short); /* same as above, but two shorts... */
typedef void (* PFVAUU)(unsigned short, unsigned short); /* same as above, but two shorts... */

typedef union {
    PFV         pfv;
    PFI         pfi;
    PFL         pfl;
    PFVAL       pfval;
    PFVAI       pfvai;
    PFVAU       pfvau;
    PFVAII      pfvaii;
    PFVAUU      pfvauu;
} FP;

Sure enough, I'm able to initialize instances of this type like so:

FP funcpointer = { .pfvai = setdata };

Clang and GCC don't complain. Is this a bad idea?

2 Answers

What you're doing is valid. As long as you're calling the pointed-to function via the proper pointer type, it's well defined.

This union could get big however depending on how many different function types you have to support, and you have to keep it in sync with your set of typedefs. As it turns out you can freely convert from one function pointer type to another via a cast, you just need to make sure you call it with the proper type.

Section 6.3.2.3p8 of the C standard say the following about function pointer conversions:

A pointer to a function of one type may be converted to a pointer to a function of another type and back again; the result shall compare equal to the original pointer. If a converted pointer is used to call a function whose type is not compatible with the referenced type, the behavior is undefined.

So you could also just use void (*)() as a generic pointer type instead of using a union, then you would need to apply the proper cast when you call it. For example:

typedef void (*FP)();
FP fp = setdata;
...
((PFVAI)fp)(123);

This can be fun, and I've used the pattern below plenty of times in pure C projects.

The idea is to collect all information about what you want to set up in a single "table", and then run the contents of this table through various macro "functions".

#define WITH_FUNP_MEMBERS \
  ON_FUNP_MEMBER(0, PFV, void, void) \
  ON_FUNP_MEMBER(1, PFL, long, void) \
  ON_FUNP_MEMBER(2, PFS, short, void) \
  ON_FUNP_MEMBER(3, PFI, int, void) \
  ON_FUNP_MEMBER(4, PFVL, void, long) \
  ON_FUNP_MEMBER(5, PFVS, void, short) \
  ON_FUNP_MEMBER(6, PFVI, void, int) \
  ON_FUNP_MEMBER(7, PFVSS, void, short, short)

// NAME_tag: Tags
enum Type_Tag { void_tag = 0, long_tag, short_tag, int_tag };
#define ON_FUNP_MEMBER(id, NAME, ret, ...) NAME##_tag = ret##_tag << 8 | id,
enum FUNP_Tag { WITH_FUNP_MEMBERS };
#undef ON_FUNP_MEMBER

// NAME_ret: Return Types
#define ON_FUNP_MEMBER(id, NAME, ret, ...) typedef ret NAME##_ret;
WITH_FUNP_MEMBERS
#undef ON_FUNP_MEMBER

// NAME: Function Pointer Types
#define ON_FUNP_MEMBER(id, NAME, ret, ...) typedef ret(*NAME)(__VA_ARGS__);
WITH_FUNP_MEMBERS
#undef ON_FUNP_MEMBER

// Functor
#define ON_FUNP_MEMBER(id, NAME, ret, ...) NAME p_##NAME;
struct {
    union {
        WITH_FUNP_MEMBERS // PFV p_PFV; PFL p_PFL; ...
    };
    enum FUNP_Tag tag;
} typedef Functor;
#undef ON_FUNP_MEMBER

Now we can produce some type-safe setters, where the type of the function being assigned is captured in the name of the setter. The compiler will warn if a function of the wrong type is assigned.

// Setter Declarations
#define ON_FUNP_MEMBER(id, NAME, ret, ...) \
    void FUNP_Set_##NAME(Functor *f, NAME arg);    
WITH_FUNP_MEMBERS
#undef ON_FUNP_MEMBER

// Setter Definitions
#define ON_FUNP_MEMBER(id, NAME, ret, ...) \
    void FUNP_Set_##NAME(Functor *f, NAME arg) { \
        f->tag = NAME##_tag; \
        f->p_##NAME = arg; \
    }
WITH_FUNP_MEMBERS
#undef ON_FUNP_MEMBER

Now we can declare a convenient functor initializer, and the invoker. The invoker is type-safe, and will not call the functor if its type doesn't match the NAME passed to invoker.

// Invoker Helpers
static inline int FUNP__set_ok(void* ok) { if (ok) *(int*)ok = 1; }
static inline int FUNP__clr_ok(void *ok) { if (ok) *(int*)ok = 0; }

// Functor Initializer
// Example: Functor f = FUNP_INIT(PFI, my_int_function);
#define FUNP_INIT(NAME, function) {.p_##NAME = (function), .tag = NAME##_tag}

// Functor Invoker
// Example: return FUNP_INVOKE(PFI, &f, NULL)   [returns an integer]
#define FUNP_INVOKE(NAME, f, ok, ...) \
    (((f) && (f)->tag == NAME##_tag && (f)->p_##NAME) ? (FUNP__set_ok(ok), (f)->p_##NAME(__VA_ARGS__)) : (NAME##_ret)FUNP__clr_ok(ok))

And finally:

// Test
#include <assert.h>
#include <stdio.h>

int fun1(void) { return 42; }
void fun2(int i) { printf("%d\n", i); }
int main()
{
    Functor f = FUNP_INIT(PFI, fun1);
    int value = FUNP_INVOKE(PFI, &f, NULL);
    assert(value == 42);
    int ok = 0;
    FUNP_INVOKE(PFI, &f, &ok);
    assert(ok);
    FUNP_INVOKE(PFVI, &f, &ok, value); // type mismatch
    assert(!ok);

    FUNP_Set_PFVI(&f, fun2);
    FUNP_INVOKE(PFVI, &f, NULL, value);
}
Related