【问题标题】:Emulating std::bind in C在 C 中模拟 std::bind
【发布时间】:2013-10-03 11:19:11
【问题描述】:

我使用 std::bind 来提供回调,同时通过首先绑定一些参数来抽象一些逻辑。即

void start() {

    int secret_id = 43534;

    //Bind the secret_id to the callback function object
    std::function<void(std::string)> cb = std::bind(&callback, secret_id, std::placeholders::_1);

    do_action(cb);

}

void do_action(std::function<void(std::string)> cb) {

    std::string result = "hello world";
    //Do some things...

    //Call the callback
    cb(result);
}

void callback(int secret_id, std::string result) {

    //Callback can now do something with the result and secret_id

}

所以在上面的例子中,do_action 不需要知道 secret_id 并且其他函数可以在没有自己的 secret_id 的情况下重用它。这在 do_action 是某种异步操作时特别有用。

我的问题是,有没有办法只使用 C 将参数值绑定到函数指针?

如果不是通过模拟 std::bind 那么是否有另一种方法可以将数据从 first() 传递到 callback() 而不会使中性 do_action() 复杂化?

【问题讨论】:

    标签: c++ c c++11 function-pointers


    【解决方案1】:

    没有。 C 不允许你直接这样做。

    在 C 中处理回调的标准方法是使用上下文指针:

    void register_callback(void (*cback)(void *context, int data),
                           void *context);
    

    这意味着您将传递一个函数,该函数将接受一个void * 以及回调应处理的正常参数(在上述情况下为整数),并且您还将传递一个您想要的void *传回去。

    这个void * 通常指向一个struct,它将包含您在回调中需要的所有额外参数或数据,并且使用这种方法,库不依赖于这个上下文是什么。如果回调不需要任何上下文,您只需将 NULL 指针作为 context 传递,并在从库调用时忽略第一个参数。

    有点骇人听闻且形式上不安全但有时会这样做的情况是,如果上下文是适合void *(例如整数)大小的简单数据,并且您的环境不会遇到问题你可以通过传递一个只是整数的假void * 来欺骗库,然后在从库中调用时将其转换回整数(这样可以避免调用者分配上下文并管理其生命周期)。

    关于如何欺骗语言以避免这种限制(仍然停留在可移植 C 的领域)我可以想到一些 hack:

    首先我们分配一个包含两个参数的回调和上下文数据池

    void (*cbf[6])(int, int);
    int ctx[6];
    

    然后我们编写(或宏生成)我们希望注册的函数并将调用两个参数版本。

    void call_with_0(int x) { cbf[0](ctx[0], x); }
    void call_with_1(int x) { cbf[1](ctx[1], x); }
    void call_with_2(int x) { cbf[2](ctx[2], x); }
    void call_with_3(int x) { cbf[3](ctx[3], x); }
    void call_with_4(int x) { cbf[4](ctx[4], x); }
    void call_with_5(int x) { cbf[5](ctx[5], x); }
    

    我们还将它们存储在分配和释放它们的池中:

    int first_free_cback = 0;
    int next_free_cback[6] = {1, 2, 3, 4, 5, -1};
    
    void (*cbacks[6])(int) = { call_with_0,
                               call_with_1,
                               call_with_2,
                               call_with_3,
                               call_with_4,
                               call_with_5 };
    

    然后绑定第一个参数,我们可以这样做

    void (*bind(void (*g)(int, int), int v0))(int)
    {
        if (first_free_cback == -1) return NULL;
        int i = first_free_cback;
        first_free_cback = next_free_cback[i];
        cbf[i] = g; ctx[i] = v0;
        return cbacks[i];
    }
    

    但绑定函数也必须显式释放

    int deallocate_bound_cback(void (*f)(int))
    {
        for (int i=0; i<6; i++) {
            if (f == cbacks[i]) {
                next_free_cback[i] = first_free_cback;
                first_free_cback = i;
                return 1;
            }
        }
        return 0;
    }
    

    【讨论】:

      【解决方案2】:

      正如 6502 所解释的,如果没有将某种上下文参数传递给回调,即使它没有直接命名 secret_id,也无法在可移植 C 中执行此操作。但是,有诸如Bruno Haible's trampoline 之类的库可以通过不可移植的方式创建带有附加信息(闭包)的 C 函数。这些库通过调用程序集或编译器扩展来发挥作用,但它们被移植到许多流行的平台上;如果它们支持关心的架构,它们就可以正常工作。

      取自the web,下面是蹦床启用的代码示例,这是一个接受参数abc的高阶函数(类似于您的secret_id,并返回一个计算a*x^2 + b*x + c的恰好一个参数x的函数:

      #include <trampoline.h>
      
      static struct quadratic_saved_args {
          double a;
          double b;
          double c;
      } *quadratic_saved_args;
      
      static double quadratic_helper(double x) {
          double a, b, c;
          a = quadratic_saved_args->a;
          b = quadratic_saved_args->b;
          c = quadratic_saved_args->c;
          return a*x*x + b*x + c;
      }
      
      double (*quadratic(double a, double b, double c))(double) {
          struct quadratic_saved_args *args;
          args = malloc(sizeof(*args));
          args->a = a;
          args->b = b;
          args->c = c;
          return alloc_trampoline(quadratic_helper, &quadratic_saved_args, args);
      }
      
      int main() {
          double (*f)(double);
          f = quadratic(1, -79, 1601);
          printf("%g\n", f(42));
          free(trampoline_data(f));
          free_trampoline(f);
          return 0;
      }
      

      【讨论】:

      • 闭包确实是在“C”中实现这一点的方法。 LibFFI 提供闭包并可移植到大多数主要操作系统:sourceware.org/libffi
      • @BojanNikolic 好点,而且 libffi 比蹦床更受欢迎。一个使用 libffi 解决 OP 问题的示例的答案可能会受到赞赏。
      【解决方案3】:

      简短的回答是否定的。

      您唯一能做的就是声明另一个内置了secret_id 的函数。如果您使用的是 C99 或更新版本,则可以将其设为内联函数,以至少限制函数调用开销,尽管较新的编译器可能会自行执行此操作。

      坦率地说,这就是 std::bind 所做的一切,因为它返回一个模板结构,std::bind 只是声明了一个内置了 secret_id 的新仿函数。

      【讨论】:

        【解决方案4】:

        不透明类型并在源中保密应该这样做:

        #include <stdio.h>
        
        // Secret.h
        
        typedef struct TagSecret Secret;
        typedef void (*SecretFunction)(Secret*, const char* visible);
        void secret_call(Secret*, const char* visible);
        
        // Public.c
        
        void public_action(Secret* secret, const char* visible) {
            printf("%s\n", visible);
            secret_call(secret, visible);
        }
        
        
        // Secret.c
        
        struct TagSecret {
            int id;
        };
        
        void secret_call(Secret* secret, const char* visible) {
            printf("%i\n", secret->id);
        }
        
        void start() {
            Secret secret = { 43534 };
            public_action(&secret, "Hello World");
        }
        
        
        int main() {
            start();
            return 0;
        }
        

        (以上不涉及注册回调函数)

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2010-09-15
          • 1970-01-01
          • 2012-05-20
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-01-21
          • 2016-04-26
          相关资源
          最近更新 更多