A friend once asked me a question like this:
I’m a little confused about the purpose of
f_ctx
. Here,f
is a handler function that gets called when the event is triggered, andf_ctx
is − according to the documentation – some pointer argument that gets passed tof
whenever it gets called. Why do we needf_ctx
? Wouldn’tf
alone suffice?
This is a trick for low-level languages like C where functions are represented using a raw function pointer, which does not store an enclosing environment (sometimes called a context). It is not needed in higher-level languages with support for first-class functions, such as Python, as these languages allow functions to be nested inside other functions and will automatically store the enclosing environment within the function objects in a combination called a closure.
The need for an environment pointer f_ctx
arises when you want to write a function that depends on external parameters not known at compile time. The f_ctx
parameter allows you to smuggle these external parameters into f
however you like.
It might be best to illustrate this with an example. Consider a 1-dimensional numerical integrator like this:
This works fine if you know the complete form of the function f
ahead of time. But what if this is not the case – what if the function requires parameters? Say we want to calculate the gamma function using an integral:
double integrand(double x)
{
double t = /* where do we get "t" from?? */;
return pow(x, t - 1.0) * exp(-x);
}
double gamma_function(double t)
{
/* how do we send the value of "t" into "integrand"? */
return integrate_1(&integrand, 0.0, INFINITY) / M_PI;
}
Using integrand_1
there are only three ways to do this:
Store
t
into a global variable, sacrificing thread safety. It would be bad to simultaneously callgamma_function
from different threads as they will both attempt to use the same global variable.Use a thread-local variable, a feature not available until C11. At least it is thread-safe now, but it is still not reentrant.
Write raw machine code to create an integrand on the fly. This can be implemented in a thread-safe and reentrant manner, but it is both inefficient, unportable, and inhibits compiler optimizations.
However, if the numerical integrator were to be re-designed like this:
double integrate_2(
double (*f)(void *f_ctx, double x),
void *f_ctx, /* passed into every invocation of "f" */
double x1,
double x2
);
Then there is a much simpler solution that avoids all of these problems:
double integrand(void *ctx, double x)
{
double t = *(double *)ctx;
return pow(x, t - 1.0) * exp(-x);
}
double gamma_function(double t)
{
return integrate_2(&integrand, &t, 0.0, INFINITY) / M_PI;
}
This is thread-safe, reentrant, efficient, and portable.
As mentioned earlier, this problem does not exist in languages like Python where functions can be nested inside other functions (or rather, it is automatically taken care of by the language itself):
Show Disqus comments
comments powered by Disqus