将函数指针与环境联系起来的实例

91 阅读2分钟

一个朋友曾经问过我这样一个问题:

void register_event_handler(void *f_ctx, void (*f)(void *ctx));

我对f_ctx 的作用有点困惑。在这里,f 是一个处理函数,当事件被触发时被调用,而f_ctx 是--根据文档--一些指针参数,每当它被调用时被传递给f 。为什么我们需要f_ctx ?光是f 不就够了吗?

这是像 C 这样的低级语言的技巧,在这些语言中,函数是用一个原始的函数指针来表示的,它并不存储一个包围的环境(有时称为上下文)。在高级语言中,它不需要支持 第一类函数的支持,例如 Python,因为这些语言允许将函数嵌套在其他函数中,并且会自动将包围环境存储在函数对象中,这种组合称为闭包

当你想写一个依赖于在编译时不知道的外部参数的函数时,就需要一个环境指针f_ctxf_ctx 参数允许你以你喜欢的方式将这些外部参数偷渡到f

用一个例子来说明这一点可能是最好的,考虑一个像这样的一维数值积分器:

double integrate_1(
    double (*f)(double x), /* function to be integrated */
    double x1,
    double x2
);

如果你提前知道函数的完整形式f ,这就很好用。但如果不是这样呢--如果该函数需要参数呢?比如我们想用积分来计算伽马函数

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;
}

使用integrand_1 ,只有三种方法可以做到这一点:

  1. t 存储到一个全局变量中,牺牲了线程安全。从不同的线程同时调用gamma_function ,这是很糟糕的,因为它们都会试图使用同一个全局变量。

  2. 使用一个线程本地变量,这个特性在C11之前是不存在的。至少它现在是线程安全的,但它仍然不是可重入的

  3. 编写原始的机器代码,在运行中创建一个积分。这可以用线程安全和可重入的方式来实现,但它既没有效率,也不便于携带,还抑制了编译器的优化。

然而,如果数值积分器要像这样重新设计:

double integrate_2(
    double (*f)(void *f_ctx, double x),
    void *f_ctx, /* passed into every invocation of "f" */
    double x1,
    double x2
);

那么有一个更简单的解决方案可以避免所有这些问题:

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;
}

这是线程安全的、可重入的、高效的和可移植的。

如前所述,这个问题在Python这样的语言中并不存在,在这种语言中,函数可以嵌套在其他函数中(或者说,它被语言本身自动处理了):

def gamma_function(t):
    def integrand(x):
        return x ** (t - 1.0) * math.exp(-x)
    return integrate(integrand, 0.0, math.inf) / math.pi