There are two main use cases for use with indirect functions:
closures and
callbacks.
A typical example of callbacks is how the standard C
qsort() function takes the (pointer to the) function used to compare elements as a parameter.
In this case, free_electron has a closure: a function pointer, and an
environment (here, parameters) the function is called in. Unlike some other programming languages, C and C++ do not have a native way to describe arbitrary closures; this is why structures (or classes in C++) are used to describe them. In C++, subclasses can be used to describe function calls with different types and/or number of arguments. Unions can be used in both C and C++ (especially in code that compiles as either C or C++ cleanly), but the aliasing rules does impose certain constraints on how this should be done: commonly used code
works but may not be portable (see e.g. POSIX
struct sockaddr, which only works with compilers and C libraries that implement the stricter POSIX rules, and not just standard C) – and obviously that is also the kinda sorta the situation we have here.
The Arduino environment is a freestanding GNU C/C++ environment, with a custom system library (the base Arduino library). The standard C or C++ libraries are not available. Because the compiler used is GNU gcc, the rules it implements do differ a bit from the standard C and C++ specifications. Language lawyers can discuss whether it
conforms to which version of which standard, but basically, C++ freestanding environment leaves a lot for the implementation to decide, whereas the C freestanding environment is a bit better specified; and the freestanding environment GNU C and C++ compilers provide is pretty well described
here and piecewise in the run time options. In particular, there are
built in functions that although nominally part of the C or C++ library, are by default implemented by the compiler instead, unless directed otherwise; and that the prototype
int func() can actually mean either
"function with arbitrary arguments" or
"function with no arguments", depending on the exact details.
Wait, keep reading: I do have a point. You see, because of the above, the solution here is to use the minimum intersection of all of the above, to get it to
work in real life in different environments. (In language lawyer terms, this is about picking the most likely subset of the implementation defined behaviour, plus assumed workarounds for bugs in non-conforming compilers.)
- Use exact function pointer declaration in the structure, not via a typedef
- Use explicit function prototypes
- For different types of functions, use an initial common member in the structure to describe the type via an enum, and an union of the structures containing the parameters passed to each type of function
The first one is arguably a bug in a compiler, but
. The second avoids the issue with ambiguity in a function prototype with empty parameter list (whether that means "arbitrary parameters" or "no parameters" or something else, depends; and we want to avoid any ambiguity to get the code to work). The third one works in both C and C++ even with strict aliasing rules. But if you only need closures of one specific function prototype type, you don't need to bother.
Let's say you want to support closures that have a context of two integer parameters, and a third one supplied at call time, so something that is ambiguous wrt. the above terminology just to keep things fun; and returns an int:
struct afterthought {
int (*callback)(int a, int b, int c);
int a;
int b;
};
#define AT_NUM_X 5
#define AT_NUM_Y 6
static struct afterthought at_array[AT_NUM_Y][AT_NUM_X];
int set_afterthought(int x, int y, int (*callback)(int a, int b, int c), int a, int b) {
if (x < 0 || x >= AT_NUM_X || y < 0 || y >= AT_NUM_Y) {
/* Index [y][x] is out of bounds */
return -1;
}
at_array[y][x].callback = callback;
at_array[y][x].a = a;
at_array[y][x].b = b;
return 0;
}
int run_afterthought(int x, int y, int c) {
if (x < 0 || x >= AT_NUM_X || y < 0 || y >= AT_NUM_Y) {
/* Index [y][x] is out of bounds */
return -1;
}
return at_array[y][x].callback(at_array[y][x].a, at_array[y][x].b, c);
}
The index check is not an assert(), because assert() is not available in freestanding C or C++. We could write a macro, or use
#ifndef NDEBUG ... #endif around the index bounds check, so it is only compiled in when
NDEBUG is NOT defined, just like assert()s.
When we supply the function to use to
set_afterthought(), you may or may not need to use ampersand (address-of operator). In C, the two are equivalent, but I'm not sure how C-and-C++ compilers (like the Microsoft compiler) treat it. You could work around this via a macro (like Glib uses
G_CALLBACK(function)), so you only need to pick it once based on the compiler used (per
macros that each compiler defines).