Wrapping C-callbacks with C ++ lambdas, is it possible to use template polymorphism?

Ok, I posted a few questions that have recently been linked to ending the C callback API with the 11th C ++ interface. I almost have a satisfactory solution, but I think it may be more elegant and need the help of some template metaprogramming wizards :)

Bear with me, as the sample code is a bit long, but I tried to demonstrate the problem with one shot. Basically, the idea is that, given the list of function pointers and data context pointers, I want to provide a callback mechanism that can be provided with

  • Function Pointers
  • Function Objects (Functors)
  • Lambda

In addition, I want various prototypes to call these functions. I mean, the C API contains about 7 different parameters for a callback, but in most cases, the user code is really only interested in one or two of them. Therefore, I would like the user to be able to specify only those arguments that are of interest to him. (This extends from the point of allowing lambdas first of all ... to provide brevity.)

In this example, the nominal callback C accepts the parameter intand floatand optional float*, which can be used to return some additional data. Thus, the intention of C ++ code is to provide a callback to any of these prototypes in any form that is “callable”. (e.g. functor, lambda, etc.).

int callback2args(int a, float b);
int callback3args(int a, float b, float *c);

Here is my solution.

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

, , . , /lambdas std::function inelegant. , , , , . , fcall2args , fcall3args add_callback . , , -.

The second problem is that I, of course, make copies of functor / lambda objects using new, but not deleteusing this memory. I'm not sure the best way would be to keep track of these distributions, although I assume that in a real implementation I could keep track of them in the object of which I am add_callbacka member, and free them in the destructor.

Thirdly, I do not find it very elegant to have specific types call2args, call3argsetc. for every variation of the callback I want to allow. This means that I will need a type explosion for each combination of parameters that the user may need. I was hoping there might be some kind of boilerplate solution to make this more general, but I am having problems with this.

. std::vector<std::pair<callback*,void*>> callbacks , . , , , ++ , 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);

callback ,

typedef int callback(int a,float b,float *c, void *context);

. , "someobject" - , .. , .

C. , , , , ++ . , ++ lambdas . , ++, :

add_object_callback(struct someobject *obj, func);

func :

  • C, context.
  • -
  • lambda

, , // :

int cb2args(int a, float b);
int cb2args(int a, float b, float *c);

, , 80% , . , . , function_traits - , . , C, , , ++.

+5
1

C API ++ 11, ++. , , .

, . call<*>, add_callback s.

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

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

( ideone , g++ 4.5 , , .)

#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();
}
+4

All Articles