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

2

10

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?

Donley answered 19/11, 2020 at 20:41 Comment(12)
As long as you can remember which one you have assigned it should work ok.Worthwhile
Though I cannot think to a scenario in which I would need something like that, in my current project, I congratulate you because it is a very clever and elegant solution. All I can suggest is that, as in many cases in which a union is used, I recommend to store yours in a struct along with another field containing the function type. It could be an enum like enum {PFV_ID, PFI_ID, PFL_ID, PFVAL_ID, PFVAI_ID, PFVAU_ID, PFVAII_ID, PFVAUU_ID} pfType;.Salsify
And if you are not afraid of heavy uses of preprocessor macros, there's a way to define them altogether and keep them all aligned.Salsify
How are you going to call it?Samul
@Samul In the calling context, it's understood what the function is meant to be. You would call funcpointer.pfvai(newdata).Donley
There is no need. Any pointer to function can be cast to any other and back, with no ill effects whatsoever. You can use void(*)(void) as your generic pointer to function type, exactly as you would use void* as a generic data pointer type.Dominicadominical
@FredFrey why not just use PFVAI instead of the union then?Samul
@n.'pronouns'm. well, not "exactly" as you need a cast to make the conversion. I guess OP's motivation is finding casts unaestheticSamul
@n.'pronouns'm. Does the standard guarantee that, or does it just happen to work on common platforms?Prude
@Samul There's multiple call sites in the code, but at any particular call site, it's understood which function it's meant to be.Prude
My goal was to easily associate "strings" with "functions" so that I could throw them around as callbacks. Once the callbacks get to where they are going to be called from, it's understood what they are.Donley
@JosephSible-ReinstateMonica port70.net/~nsz/c/c11/n1570.html#6.3.2.3p8Dominicadominical
R
4

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);
Romantic answered 19/11, 2020 at 22:28 Comment(1)
Using a chosen function pointer type instead of a union and casting as appropriate is definitely the way I would go. Indeed, I would have written pretty much this answer if I hadn't been beaten to it. The union isn't necessarily a bad idea, but I rate it less good.Masonmasonic
I
1

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);
}
Intestate answered 19/11, 2020 at 22:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.