さて、最近、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
次のいずれかです。
- を使用しない通常の C 関数
context
。 - ファンクターオブジェクト
- ラムダ
さらに、いずれの場合も、関数/ファンクター/ラムダが次のシグネチャのいずれかを持つことが可能である必要があります。
int cb2args(int a, float b);
int cb2args(int a, float b, float *c);
私はこれが可能であるべきだと思います。私はそこまでの約 80% を達成しましたが、呼び出しシグネチャに基づくテンプレートのポリモーフィズムに行き詰まっています。それが可能かどうかはわかりません。たぶん、ブードゥー教function_traits
の何かが必要なのかもしれませんが、それは私の経験を少し超えています. いずれにせよ、このようなインターフェースを使っている C ライブラリは非常に多いので、C++ から使う場合にこのような便利さを提供できるとよいと思います。