使用C++ lambdas包装C回调,可能使用模板多态?

Ste*_*eve 5 c++ lambda templates metaprogramming c++11

好的,我最近发布了一些与使用C++ 11-ish接口包装C回调API相关的问题.我几乎有一个令人满意的解决方案,但我认为它可能更优雅,需要一些模板元编程向导的帮助:)

请耐心等待,因为示例代码有点长,但我试图一次性演示这个问题.基本上,我的想法是,给定一个函数指针和数据上下文指针的列表,我想提供一个可以提供的回调机制,

  • 函数指针
  • 函数对象(仿函数)
  • Lambda表达式

此外,我想通过各种原型使这些函数可调用.我的意思是,C API为回调提供了大约7个不同的参数,但在大多数情况下,用户代码实际上只对其中的一个或两个感兴趣.所以我希望用户能够只指定他感兴趣的参数.(这从最初允许lambdas的角度来看......允许简洁.)

在此示例中,标称C回调采用a int和a float参数,并且可选float*用于返回一些额外数据.因此,C++代码的目的是能够以"可调用"的任何形式提供任何这些原型的回调.(例如,仿函数,lambda等)

int callback2args(int a, float b);
int callback3args(int a, float b, float *c);
Run Code Online (Sandbox Code Playgroud)

这是我到目前为止的解决方案.

#include <cstdio>
#include <vector>
#include <functional>

typedef int call2args(int,float);
typedef int call3args(int,float,float*);
typedef std::function<call2args> fcall2args;
typedef std::function<call3args> fcall3args;

typedef int callback(int,float,float*,void*);
typedef std::pair<callback*,void*> cb;

std::vector<cb> callbacks;

template <typename H>
static
int call(int a, float b, float *c, void *user);

template <>
int call<call2args>(int a, float b, float *c, void *user)
{
    call2args *h = (call2args*)user;
    return (*h)(a, b);
}

template <>
int call<call3args>(int a, float b, float *c, void *user)
{
    call3args *h = (call3args*)user;
    return (*h)(a, b, c);
}

template <>
int call<fcall2args>(int a, float b, float *c, void *user)
{
    fcall2args *h = (fcall2args*)user;
    return (*h)(a, b);
}

template <>
int call<fcall3args>(int a, float b, float *c, void *user)
{
    fcall3args *h = (fcall3args*)user;
    return (*h)(a, b, c);
}

template<typename H>
void add_callback(const H &h)
{
    H *j = new H(h);
    callbacks.push_back(cb(call<H>, (void*)j));
}

template<>
void add_callback<call2args>(const call2args &h)
{
    callbacks.push_back(cb(call<call2args>, (void*)h));
}

template<>
void add_callback<call3args>(const call3args &h)
{
    callbacks.push_back(cb(call<call3args>, (void*)h));
}

template<>
void add_callback<fcall2args>(const fcall2args &h)
{
    fcall2args *j = new fcall2args(h);
    callbacks.push_back(cb(call<fcall2args>, (void*)j));
}

template<>
void add_callback<fcall3args>(const fcall3args &h)
{
    fcall3args *j = new fcall3args(h);
    callbacks.push_back(cb(call<fcall3args>, (void*)j));
}

// Regular C-style callback functions (context-free)
int test1(int a, float b)
{
    printf("test1 -- a: %d, b: %f", a, b);
    return a*b;
}

int test2(int a, float b, float *c)
{
    printf("test2 -- a: %d, b: %f", a, b);
    *c = a*b;
    return a*b;
}

void init()
{
    // A functor class
    class test3
    {
    public:
        test3(int j) : _j(j) {};
        int operator () (int a, float b)
        {
            printf("test3 -- a: %d, b: %f", a, b);
            return a*b*_j;
        }
    private:
        int _j;
    };

    // Regular function pointer of 2 parameters
    add_callback(test1);

    // Regular function pointer of 3 parameters
    add_callback(test2);

    // Some lambda context!
    int j = 5;

    // Wrap a 2-parameter functor in std::function
    add_callback(fcall2args(test3(j)));

    // Wrap a 2-parameter lambda in std::function
    add_callback(fcall2args([j](int a, float b)
                 {
                     printf("test4 -- a: %d, b: %f", a, b);
                     return a*b*j;
                 }));

    // Wrap a 3-parameter lambda in std::function
    add_callback(fcall3args([j](int a, float b, float *c)
                 {
                     printf("test5 -- a: %d, b: %f", a, b);
                     *c = a*b*j;
                     return a*b*j;
                 }));
}

int main()
{
    init();

    auto c = callbacks.begin();
    while (c!=callbacks.end()) {
        float d=0;
        int r = c->first(2,3,&d,c->second);
        printf("  result: %d (%f)\n", r, d);
        c ++;
    }
}
Run Code Online (Sandbox Code Playgroud)

好的,正如你所看到的,这确实有效.但是,我发现必须明确地将functor/lambdas包装为std::function类型不合适的解决方案.我真的想让编译器自动匹配函数类型,但这似乎不起作用.如果我删除3参数变量,则不需要fcall2args包装器,但是版本的存在使得编译器显然不明确.换句话说,它似乎无法基于lambda调用签名进行模式匹配.fcall3argsadd_callback

第二个问题是我当然正在使用new,但不是使用delete这个内存来复制functor/lambda对象.我现在还不确定跟踪这些分配的最佳方法,虽然我想在一个真实的实现中我可以在add_callback一个成员的对象中跟踪它们,并在析构函数中释放它们.

第三,我不觉得它很优雅有特定类型的call2args,call3args等等,因为我想允许回调的每个变化.这意味着我需要为用户可能需要的每个参数组合添加类型.我希望有一些模板解决方案可以使它更通用,但我很难想出来.

编辑说明:此代码中的定义std::vector<std::pair<callback*,void*>> callbacks问题定义的一部分,而不是答案的一部分.我试图解决的问题是将C++对象映射到此接口 - 因此,提出更好的方法来组织这个std::vector并不能解决我的问题.谢谢.只是为了澄清.

编辑#2:好的,忘记我的示例代码std::vector<std::pair<callback*,void*>> callbacks用来保存回调的事实.想象一下,因为这是实际场景,我有一些C库实现以下接口:

struct someobject *create_object();
free_object(struct someobject *obj);
add_object_callback(struct someobject *obj, callback *c, void *context);
Run Code Online (Sandbox Code Playgroud)

在哪里callback,

typedef int callback(int a,float b,float *c, void *context);
Run Code Online (Sandbox Code Playgroud)

好的.因此,"someobject"将体验某种外部事件,网络数据或输入事件等,并在发生这些事件时调用其回调列表.

这是C语言中非常标准的回调实现.重要的是,这是一个现有的库,我无法改变,但我正在尝试编写一个很好的,惯用的C++包装器.我希望我的C++用户能够添加lambda作为回调.所以,我想设计一个C++接口,允许用户能够执行以下操作:

add_object_callback(struct someobject *obj, func);
Run Code Online (Sandbox Code Playgroud)

其中func一个是以下之一:

  1. 一个不使用的常规C函数context.
  2. 一个仿函数对象
  3. 一个lambda

另外,在每种情况下,函数/ functor/lambda应该可以具有以下签名之一:

int cb2args(int a, float b);
int cb2args(int a, float b, float *c);
Run Code Online (Sandbox Code Playgroud)

认为这应该是可能的,我有大约80%的方式,但我坚持基于呼叫签名的模板多态.我不知道是否有可能.也许它需要一些伏都教function_traits或其他东西,但它有点超出我的经验.在任何情况下,都有许多C库使用这样的接口,我认为从C++使用它们时允许这种方便会很棒.

ken*_*ytm 4

由于您在 C++11 中使用 C API,因此您也可以将整个内容包装在 C++ 类中。正如您在第二个问题中提到的,这也是必要的,以解决资源泄漏。

另请记住,不带捕获的 lambda 表达式可以隐式转换为函数指针。这可以删除所有的,call<*>因为它们可以移动到add_callbacks 中。

最后,我们可以使用 SFINAE 来删除fcall3args类型。这是结果。

class SomeObject {
    // The real object being wrapped.
    struct someobject* m_self;

    // The vector of callbacks which requires destruction. This vector is only a
    // memory store, and serves no purpose otherwise.
    typedef std::function<int(int, float, float*)> Callback;
    std::vector<std::unique_ptr<Callback>> m_functions;

    // Add a callback to the object. Note the capture-less lambda.
    template <typename H>
    void add_callback_impl(H&& h) {
        std::unique_ptr<Callback> callback (new Callback(std::forward<H>(h)));

        add_object_callback(m_self, [](int a, float b, float* c, void* raw_ctx) {
            return (*static_cast<Callback*>(raw_ctx))(a, b, c);
        }, callback.get());

        m_functions.push_back(std::move(callback));
    }

public:
    SomeObject() : m_self(create_object()) {}
    ~SomeObject() { free_object(m_self); }

    // We create 4 public overloads to add_callback:

    // This only accepts function objects having 2 arguments.
    template <typename H>
    auto add_callback(H&& h) -> decltype(h(1, 10.f), void()) {
        using namespace std::placeholders;
        add_callback_impl(std::bind(std::forward<H>(h), _1, _2));
    }

    // This only accepts function objects having 3 arguments.
    template <typename H>
    auto add_callback(H&& h) -> decltype(h(1, 1.0f, (float*)0), void()) {
        add_callback_impl(std::forward<H>(h));
    }

    // This only accepts function pointers. 
    void add_callback(int(*h)(int, float)) const {
        add_object_callback(m_self, [](int a, float b, float* c, void* d) {
            return reinterpret_cast<int(*)(int, float)>(d)(a, b);
        }, reinterpret_cast<void*>(h));
    }

    // This only accepts function pointers.
    void add_callback(int(*h)(int, float, float*)) const {
        add_object_callback(m_self, [](int a, float b, float* c, void* d) {
            return reinterpret_cast<int(*)(int, float, float*)>(d)(a, b, c);
        }, reinterpret_cast<void*>(h));
    }

    // Note that the last 2 overloads violates the C++ standard by assuming
    // sizeof(void*) == sizeof(func pointer). This is valid in POSIX, though.

    struct someobject* get_raw_object() const {
        return m_self;
    }
};
Run Code Online (Sandbox Code Playgroud)

所以init()变成:

void init(SomeObject& so) {
    // A functor class
    class test3 { ... };

    so.add_callback(test1);
    so.add_callback(test2);

    // Some lambda context!
    int j = 5;

    so.add_callback(test3(j));
    so.add_callback([j](int a, float b) -> int {
        printf("test4 -- a: %d, b: %f", a, b);
        return a*b*j;
    });

    so.add_callback([j](int a, float b, float *c) -> int {
        printf("test5 -- a: %d, b: %f", a, b);
        *c = a*b*j;
        return a*b*j;
    });
}
Run Code Online (Sandbox Code Playgroud)

完整的测试代码(我不会将其放在 ideone 中,因为 g++ 4.5 不支持将 lambda 隐式转换为函数指针,也不支持基于范围的 for。)

#include <vector>
#include <functional>
#include <cstdio>
#include <memory>

struct someobject;
struct someobject* create_object(void);
void free_object(struct someobject* obj);
void add_object_callback(struct someobject* obj,
                         int(*callback)(int, float, float*, void*),
                         void* context);

class SomeObject {
    // The real object being wrapped.
    struct someobject* m_self;

    // The vector of callbacks which requires destruction. This vector is only a
    // memory store, and serves no purpose otherwise.
    typedef std::function<int(int, float, float*)> Callback;
    std::vector<std::unique_ptr<Callback>> m_functions;

    // Add a callback to the object. Note the capture-less lambda.
    template <typename H>
    void add_callback_impl(H&& h) {
        std::unique_ptr<Callback> callback (new Callback(std::forward<H>(h)));

        add_object_callback(m_self, [](int a, float b, float* c, void* raw_ctx) {
            return (*static_cast<Callback*>(raw_ctx))(a, b, c);
        }, callback.get());

        m_functions.push_back(std::move(callback));
    }

public:
    SomeObject() : m_self(create_object()) {}
    ~SomeObject() { free_object(m_self); }

    // We create 4 public overloads to add_callback:

    // This only accepts function objects having 2 arguments.
    template <typename H>
    auto add_callback(H&& h) -> decltype(h(1, 10.f), void()) {
        using namespace std::placeholders;
        add_callback_impl(std::bind(std::forward<H>(h), _1, _2));
    }

    // This only accepts function objects having 3 arguments.
    template <typename H>
    auto add_callback(H&& h) -> decltype(h(1, 1.0f, (float*)0), void()) {
        add_callback_impl(std::forward<H>(h));
    }

    // This only accepts function pointers. 
    void add_callback(int(*h)(int, float)) const {
        add_object_callback(m_self, [](int a, float b, float* c, void* d) {
            return reinterpret_cast<int(*)(int, float)>(d)(a, b);
        }, reinterpret_cast<void*>(h));
    }

    // This only accepts function pointers.
    void add_callback(int(*h)(int, float, float*)) const {
        add_object_callback(m_self, [](int a, float b, float* c, void* d) {
            return reinterpret_cast<int(*)(int, float, float*)>(d)(a, b, c);
        }, reinterpret_cast<void*>(h));
    }

    // Note that the last 2 overloads violates the C++ standard by assuming
    // sizeof(void*) == sizeof(func pointer). This is required in POSIX, though.

    struct someobject* get_raw_object() const {
        return m_self;
    }
};

//------------------------------------------------------------------------------

int test1(int a, float b) {
    printf("test1 -- a: %d, b: %f", a, b);
    return a*b;
}

int test2(int a, float b, float *c) {
    printf("test2 -- a: %d, b: %f", a, b);
    *c = a*b;
    return a*b;
}

void init(SomeObject& so) {
    // A functor class
    class test3
    {
    public:
        test3(int j) : _j(j) {};
        int operator () (int a, float b)
        {
            printf("test3 -- a: %d, b: %f", a, b);
            return a*b*_j;
        }

    private:
        int _j;
    };

    so.add_callback(test1);
    so.add_callback(test2);

    // Some lambda context!
    int j = 5;

    so.add_callback(test3(j));
    so.add_callback([j](int a, float b) -> int {
        printf("test4 -- a: %d, b: %f", a, b);
        return a*b*j;
    });

    so.add_callback([j](int a, float b, float *c) -> int {
        printf("test5 -- a: %d, b: %f", a, b);
        *c = a*b*j;
        return a*b*j;
    });
}

//------------------------------------------------------------------------------

struct someobject {
    std::vector<std::pair<int(*)(int,float,float*,void*),void*>> m_callbacks;
    void call() const {
        for (auto&& cb : m_callbacks) {
            float d=0;
            int r = cb.first(2, 3, &d, cb.second);
            printf("  result: %d (%f)\n", r, d);
        }
    }
};

struct someobject* create_object(void) {
    return new someobject;
}

void free_object(struct someobject* obj) {
    delete obj;
}

void add_object_callback(struct someobject* obj,
                         int(*callback)(int, float, float*, void*),
                         void* context) {
    obj->m_callbacks.emplace_back(callback, context);
}

//------------------------------------------------------------------------------

int main() {
    SomeObject so;
    init(so);
    so.get_raw_object()->call();
}
Run Code Online (Sandbox Code Playgroud)