6

さて、最近、C コールバック API を C++11 風のインターフェイスでラップすることに関連するいくつかの質問を投稿しました。私はほとんど満足のいく解決策を手に入れましたが、それはもっとエレガントで、いくつかのテンプレートメタプログラミングウィザードの助けが必要だと思います:)

サンプル コードは少し長いので、ご容赦ください。基本的には、関数ポインターとデータ コンテキスト ポインターのリストが与えられた場合に、提供できるコールバック メカニズムを提供したいと考えています。

  • 関数ポインタ
  • 関数オブジェクト (ファンクター)
  • ラムダス

さらに、これらの関数をさまざまなプロトタイプから呼び出せるようにしたいと考えています。つまり、C API は約 7 つの異なるパラメーターをコールバックに提供しますが、ほとんどの場合、ユーザー コードは実際にはこれらのうちの 1 つまたは 2 つだけに関心があります。したがって、ユーザーが興味のある引数のみを指定できるようにしたいと思います (これは、最初にラムダを許可するという点から拡張されています... 簡潔にするためです。)

この例では、公称 C コールバックはintとパラメータ、および追加のデータを返すために使用できるfloatオプションを受け取ります。float*したがって、C++ コードの意図は、これらのプロトタイプのいずれかのコールバックを、「呼び出し可能な」任意の形式で提供できるようにすることです。(ファンクタ、ラムダなど)

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

これまでの私の解決策は次のとおりです。

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

ご覧のとおり、これは実際に機能します。std::functionただし、ファンクター/ラムダをタイプとして明示的にラップする必要があるという解決策は、エレガントではありません。コンパイラに関数の型を自動的に一致させたかったのですが、うまくいかないようです。3 パラメーターのバリアントを削除すると、fcall2argsラッパーは必要ありませんがfcall3args、 のバージョンが存在add_callbackするため、コンパイラーにとって明らかにあいまいになります。つまり、ラムダ呼び出しシグネチャに基づくパターン マッチングができないようです。

2 つ目の問題は、もちろん を使用してファンクタ/ラムダ オブジェクトのコピーを作成しているが、このメモリを ing してnewいないことです。delete現時点では、これらの割り当てを追跡する最善の方法が何であるかはわかりませんが、実際の実装でadd_callbackは、メンバーであるオブジェクトでそれらを追跡し、デストラクタで解放できると思います。

第 3 に、許可したいコールバックのバリエーションごとに、特定のタイプcall2args、などを使用するのはあまりエレガントではありません。call3argsこれは、ユーザーが必要とする可能性のあるパラメーターの組み合わせごとに、型の爆発が必要になることを意味します。これをより一般的にするためのテンプレートソリューションがあることを望んでいましたが、思いつくのに苦労しています。

説明のために編集: このコードの定義は、答えの一部ではなくstd::vector<std::pair<callback*,void*>> callbacks問題の定義の一部です。私が解決しようとしている問題は、C++ オブジェクトをこのインターフェイスにマップすることです。したがって、これを整理するためのより良い方法を提案std::vectorしても、問題は解決しません。ありがとう。明確にするために。

編集 #2std::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 でのコールバックの非常に標準的な実装です。重要なことは、これは既存のライブラリであり、変更できないものですが、その周りに素敵で慣用的な C++ ラッパーを書こうとしていることです。C++ ユーザーがラムダをコールバックとして追加できるようにしたいと考えています。そこで、ユーザーが次のことができるようにする C++ インターフェイスを設計したいと思います。

add_object_callback(struct someobject *obj, func);

func次のいずれかです。

  1. を使用しない通常の C 関数context
  2. ファンクターオブジェクト
  3. ラムダ

さらに、いずれの場合も、関数/ファンクター/ラムダが次のシグネチャのいずれかを持つことが可能である必要があります。

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

私はこれが可能であるべきだと思います。私はそこまでの約 80% を達成しましたが、呼び出しシグネチャに基づくテンプレートのポリモーフィズムに行き詰まっています。それが可能かどうかはわかりません。たぶん、ブードゥー教function_traitsの何かが必要なのかもしれませんが、それは私の経験を少し超えています. いずれにせよ、このようなインターフェースを使っている C ライブラリは非常に多いので、C++ から使う場合にこのような便利さを提供できるとよいと思います。

4

1 に答える 1

4

C++11 で C API を使用しているため、全体を C++ クラスでラップすることもできます。これは、2 番目の問題で述べたように、リソース リークを解決するためにも必要です。

また、キャプチャのないラムダ式は暗黙的に関数ポインターに変換できることにも注意してください。call<*>に移動できるため、これによりすべてが削除される可能性がありますadd_callback

最後に、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;
    });
}

完全なテスト コード (g++ 4.5 は、ラムダから関数ポインターへの暗黙的な変換も、範囲ベースの for もサポートしていないため、ここでは ideone には入れません。)

#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();
}
于 2012-09-08T09:04:30.110 に答える