Viability of Casting `void(*)(int*)` to `void(*)(...)`

I was thinking about the std::function<> class and I'm trying to figure out what that would look like in C. So far I got this:
1
2
3
4
typedef struct _StatefulFunc {
  void * state;
  void(*func)(void*,                                                                                                          
} StatefulFunc;

and of course I got stuck cause I can't figure out a good way of doing some sort of generic function call type. I thought about variadic arguments immediately, but I'm unsure about if it's possible to cast something like void(char) to void(...) without messing up the actual calls.


Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
void addOne(int* a)
{
  *a += 1;
}

int main()
{
  void(generic_call*)(...);
  generic_call = (void(*)(...))addOne;  // is this Undefined Behavior?

  int a = 0;
  generic_call(&a); // Or is this Undefined Behavior..?
}

Last edited on
std::function is always assigned a template type. It's not generic.

Function pointers in C are of a type based on the signature of the function, as with std::function.

For example,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct Rectangle {
    double length, height;
};

double circumfrence(const struct Rectangle* rect) {
    return 2 * rect->length * rect->height;
}

typedef double Perimeter(const struct Rectangle*);

int main() {
    struct Rectangle rect;
    Perimeter* func = circumfrence;

    rect.length = 3.2;
    rect.height = 1.7;
    func(&rect);

    return 0;
}
Function pointers in C are of a type based on the signature of the function, as with std::function.

Yeah, but that's possible because you're either in C++ and have template information to use or you're doing it for a specific type like in your example, right?
> if it's possible to cast something like void(char) to void(...) without messing up the actual calls.


Technically, it would engender undefined behaviour.

If the converted pointer is used to make a function call, the behavior is undefined (unless the function types are compatible)
https://en.cppreference.com/w/c/language/cast

Compatible types: https://en.cppreference.com/w/c/language/type#Compatible_types
To add to what's said above, you need to "know" the correct type because the calling convention, number of and type of args and processing of possible return value needs an exact match.

So if you load a function/class from a shared lib/dll, and you know the signature, then you can do the cast.

The whole point of type safety is to catch your errors at compile time.
What you want to do is completely impossible in C and C++. The languages are simply not designed to support such dynamism on function calls.
Briefly, the language would need some way to represent the types of objects as objects unto themselves (i.e. metatypes, where typeof(int) is a value of Type, and typeof(std::string) is another value of the same class), so that they can be compared. You would be making every function call much more expensive, as well.
@JLBorges
thanks that was what I was looking for.

@kbw & @helios
eheh... 😅 now I'm kinda embarrassed lol.
I was just gonna make a function like
1
2
3
4
function call_sfunc(StatefulFunc *sfn, ...)
{
  ((void(*)(...))sfn)(...); // ummmmm don't remember how to do non-templated variadic stuff apparently...
}

If it wasn't undefined........
tho @helios I'm not entirely sure what you mean. What is it you think I'm attempting to do...? My stipulations were that it would be considered undefined behavior to
cast something like void(int) to void(char), but that casting from void(int) to void(...) would be ok, given that it isn't undefined behavior. Do the first doesn't really make any sense for the reasons you give, but the second...? Idk... doesn't seem like it would be involved in such a thing. I probably would have answered my own question if I knew more about how variadic functions work but like I never got that far, ya know? lol I don't remember what the point of this entire paragraph is anymore XD
Last edited on
If you just want to cast the pointer, that's no problem. The problem comes when you try to actually call the function.

but that casting from void(int) to void(...) would be ok, given that it isn't undefined behavior. Do the first doesn't really make any sense for the reasons you give, but the second...? Idk... doesn't seem like it would be involved in such a thing.
Think about it. If a void (*)(...) could work as a generic function pointer for any function returning no values, the compiler would need to generate at call site code that is compatible with any possible function signature on the other side of the pointer.

1
2
3
4
5
6
7
8
void f(int);
void g(int, int);
typedef void (*generic_f)(...);

auto foo = (rand() % 2) ? (generic_f)f : (generic_f)g;
foo(1);
foo(1, 2);
foo(1, 2, 3);
How many values should the caller push in each call? Do f() and g() need to do anything to support this behavior? What about this?
 
foo("bar");

If you say "well, if foo points to f then you should only call it with a single int, and if it points to g you should only call it with two", then you don't really want generic pointers, you want to select between any of a finite number of pointer types, and that's easily doable with either unions or class hierarchies.
I mean I was thinking more along the lines of having a caveman compiler read this:
1
2
3
4
5
6
void func(...)

int main()
{
  func(3, 4);
}

and just blindly pushing 2 ints to the stack, no questions asked. Which is of course why I was like this is probably undefined behavior. like... like if I take your foo("bar") example, then the compiler would just push a char pointer, no questions asked. In this version of things, the actual call to a variadic function will always look exactly the same as a normal function call under the covers, and the function itself would handle the fact that it doesn't know it's own argument length...? Which seemed viable enough until just now when I remembered I had some things backwards lol um... yeah.
Last edited on
As per the standard, it is undefined behaviour. But,
Permissible undefined behavior ranges from ..., to behaving during translation or program execution in a documented manner characteristic of the environment...
http://eel.is/c++draft/intro.defs#defns.undefined


In practice, we could do something like the equivalent of this in C (using macros, minus the perfect forwarding):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
#include <cstdio>

#ifdef __unix__
    using fptr_type = void* ; // POSIX: dlsym() returns void*
#elif defined _WIN32
    #include <windows.h>
    using fptr_type = FARPROC ; // Windows:  GetProcAddress() returns int (FAR WINAPI * FARPROC) ()
#endif // __unix__

struct func
{
    template < typename FN > func( FN* fptr ) : ptr( fptr_type(fptr) ) {}

    template < typename FN, typename... ARGS > auto call( ARGS&&... args )
    { return reinterpret_cast<FN*>(ptr)( std::forward<ARGS>(args)... ) ; }

    fptr_type ptr ;
};

void foo( const char* cstr, int i ) { std::cout << "foo('" << cstr << "'," << i << ")\n" ; }
double bar( double d ) { std::cout << "bar(" << d << ")\n" ; return d*2 ; }

int main()
{
    func fn = foo ; fn.call< decltype(foo) > ( "hello", 12345 ) ;
    fn = bar ;  fn.call< decltype(bar) > ( 12345.678 ) ;
    fn = std::printf ; fn.call< decltype(std::printf) > ( "printf: '%s',%d,%f\n", "hello again", 1234, 567.89 ) ;
}

http://coliru.stacked-crooked.com/a/e1cf9f6aa9e89e1e
That's pretty interesting though it feels weird to identify UB as permissible or not in a standard even if it makes sense lol.
Topic archived. No new replies allowed.