web-dev-qa-db-ja.com

クラスメンバー関数をコールバックとして渡すにはどうすればよいですか?

関数ポインターをコールバックとして渡す必要があるAPIを使用しています。クラスからこのAPIを使用しようとしていますが、コンパイルエラーが発生しています。

コンストラクタから私がしたことは次のとおりです。

m_cRedundencyManager->Init(this->RedundencyManagerCallBack);

これはコンパイルされません-次のエラーが表示されます:

エラー8エラーC3867: 'CLoggersInfra :: RedundencyManagerCallBack':関数呼び出しに引数リストがありません。 '&CLoggersInfra :: RedundencyManagerCallBack'を使用して、メンバーへのポインターを作成します

&CLoggersInfra::RedundencyManagerCallBackを使用するよう提案しましたが、うまくいきませんでした。

これに関する提案/説明はありますか??

VS2008を使用しています。

ありがとう!!

66
ofer

メンバー関数ポインターは、「this」オブジェクト引数を予期しているため、通常の関数ポインターのように処理できないため、機能しません。

代わりに、次のように静的メンバー関数を渡すことができます。これは、この点で通常の非メンバー関数に似ています。

m_cRedundencyManager->Init(&CLoggersInfra::Callback, this);

関数は次のように定義できます

static void Callback(int other_arg, void * this_pointer) {
    CLoggersInfra * self = static_cast<CLoggersInfra*>(this_pointer);
    self->RedundencyManagerCallBack(other_arg);
}

これは簡単な質問ですが、答えは驚くほど複雑です。簡単な答えは、std :: bind1stまたはboost :: bindでしようとしていることを実行できるということです。長い答えは以下です。

コンパイラは、&CLoggersInfra :: RedundencyManagerCallBackの使用を推奨するように修正されています。まず、RedundencyManagerCallBackがメンバー関数である場合、関数自体はクラスCLoggersInfraの特定のインスタンスに属していません。クラス自体に属します。以前に静的クラス関数を呼び出したことがあれば、同じSomeClass :: SomeMemberFunction構文を使用していることに気づいたかもしれません。関数自体は特定のインスタンスではなくクラスに属するという意味で「静的」なので、同じ構文を使用します。技術的には関数を直接渡さないため、「&」が必要です。関数はC++の実際のオブジェクトではありません。代わりに、技術的には関数のメモリアドレス、つまり、関数の命令がメモリ内で始まる場所へのポインタを渡します。結果は同じですが、パラメーターとして事実上「関数を渡す」ことになります。

しかし、それはこの場合の問題の半分にすぎません。先ほど言ったように、RedundencyManagerCallBack関数は特定のインスタンスに「属しません」。しかし、特定のインスタンスを念頭に置いて、コールバックとして渡したいようです。これを行う方法を理解するには、メンバー関数が実際に何であるかを理解する必要があります。追加の非表示パラメーターを持つ通常の未定義クラス関数です。

例えば:

class A {
public:
    A() : data(0) {}
    void foo(int addToData) { this->data += addToData; }

    int data;
};

...

A an_a_object;
an_a_object.foo(5);
A::foo(&an_a_object, 5); // This is the same as the line above!
std::cout << an_a_object.data; // Prints 10!

A :: fooはいくつのパラメーターを取りますか?通常、1と言います。しかし、内部では、fooは実際に2を取ります。A:: fooの定義を見ると、「this」ポインターが意味を持つためにAの特定のインスタンスが必要です(コンパイラーは、これは)。 「this」にしたいものを通常指定する方法は、構文MyObject.MyMemberFunction()を使用することです。しかし、これは、MyObjectのアドレスをMyMemberFunctionの最初のパラメーターとして渡すための構文上の砂糖です。同様に、クラス定義内でメンバー関数を宣言する場合、パラメーターリストに「this」を入れませんが、これは入力を節約するための言語デザイナーからの贈り物です。代わりに、追加の「this」パラメーターを自動的に取得するために、メンバー関数が静的であることを指定する必要があります。 C++コンパイラーが上記の例をCコードに変換した場合(元のC++コンパイラーは実際にそのように動作しました)、おそらく次のようになります。

struct A {
    int data;
};

void a_init(A* to_init)
{
    to_init->data = 0;
}

void a_foo(A* this, int addToData)
{ 
    this->data += addToData;
}

...

A an_a_object;
a_init(0); // Before constructor call was implicit
a_foo(&an_a_object, 5); // Used to be an_a_object.foo(5);

例に戻ると、明らかな問題があります。 'Init'は、1つのパラメーターを受け取る関数へのポインターが必要です。ただし、&CLoggersInfra :: RedundencyManagerCallBackは、通常のパラメーターと秘密の「this」パラメーターの2つのパラメーターを受け取る関数へのポインターです。したがって、なぜコンパイラエラーが依然として発生するのか(補足:Pythonを使用したことがある場合、この種の混乱がすべてのメンバー関数に 'self'パラメーターが必要な理由です)。

これを処理する冗長な方法は、必要なインスタンスへのポインターを保持し、パラメーターを受け取る 'run'または 'execute'(または '()'演算子のオーバーロード)などのメンバー関数を持つ特別なオブジェクトを作成することですメンバー関数の場合は、保存されたインスタンスのパラメーターでメンバー関数を呼び出すだけです。しかし、これには「Init」を変更して生の関数ポインタではなく特別なオブジェクトを取得する必要があり、Initは他の人のコードのように聞こえます。また、この問題が発生するたびに特別なクラスを作成すると、コードが肥大化します。

最後に、boost :: bindとboost :: functionという優れたソリューションがあります。それぞれのドキュメントはここにあります。

boost :: bind docsboost :: function docs

boost :: bindを使用すると、関数とその関数のパラメーターを取得し、そのパラメーターが適切に「ロック」されている新しい関数を作成できます。したがって、2つの整数を追加する関数がある場合、boost :: bindを使用して、パラメーターの1つが5にロックされている新しい関数を作成できます。この新しい関数は整数パラメーターを1つだけ受け取り、常に5を追加しますそれに。この手法を使用すると、特定のクラスインスタンスになるように非表示の「this」パラメーターを「ロックイン」し、必要に応じて1つのパラメーターのみを受け取る新しい関数を生成できます(非表示パラメーターは常にfirstパラメータ、および通常のパラメータは順番に来ます)。例についてはboost :: bindのドキュメントをご覧ください。メンバー関数での使用について具体的に説明しています。技術的には、std :: bind1stという標準関数も使用できますが、boost :: bindの方がより一般的です。

もちろん、キャッチはもう1つあります。 boost :: bindはNice boost :: functionを作成しますが、これはまだ技術的にはInitが望むような生の関数ポインタではありません。ありがたいことに、StackOverflow here で文書化されているように、boostはboost :: functionを生のポインターに変換する方法を提供します。これをどのように実装するかは、この答えの範囲を超えていますが、それも興味深いものです。

これがばかげて難しいように思えても心配しないでください-あなたの質問はC++のいくつかの暗いコーナーと交差します。

C++ 11更新:boost :: bindの代わりに、「this」をキャプチャするラムダ関数を使用できるようになりました。これは基本的にコンパイラに同じものを生成させることです。

105
Joseph Garvin

この回答は、上記のコメントへの返信であり、VisualStudio 2008では機能しませんが、最近のコンパイラでは推奨されるはずです。


一方、ボイドポインターを使用する必要はもうありません。また、 _std::bind_ および _std::function_ が利用可能であるため、ブーストの必要もありません。 Oneの利点(voidポインターと比較した場合)は、戻り値の型と引数が_std::function_を使用して明示的に記述されているため、型の安全性です。

_// std::function<return_type(list of argument_type(s))>
void Init(std::function<void(void)> f);
_

次に、_std::bind_を使用して関数ポインターを作成し、それをInitに渡すことができます。

_auto cLoggersInfraInstance = CLoggersInfra();
auto callback = std::bind(&CLoggersInfra::RedundencyManagerCallBack, cLoggersInfraInstance);
Init(callback);
_

完全な例 メンバー、静的メンバー、および非メンバー関数で_std::bind_を使用する場合:

_#include <functional>
#include <iostream>
#include <string>

class RedundencyManager // incl. Typo ;-)
{
public:
    // std::function<return_type(list of argument_type(s))>
    std::string Init(std::function<std::string(void)> f) 
    {
        return f();
    }
};

class CLoggersInfra
{
private:
    std::string member = "Hello from non static member callback!";

public:
    static std::string RedundencyManagerCallBack()
    {
        return "Hello from static member callback!";
    }

    std::string NonStaticRedundencyManagerCallBack()
    {
        return member;
    }
};

std::string NonMemberCallBack()
{
    return "Hello from non member function!";
}

int main()
{
    auto instance = RedundencyManager();

    auto callback1 = std::bind(&NonMemberCallBack);
    std::cout << instance.Init(callback1) << "\n";

    // Similar to non member function.
    auto callback2 = std::bind(&CLoggersInfra::RedundencyManagerCallBack);
    std::cout << instance.Init(callback2) << "\n";

    // Class instance is passed to std::bind as second argument.
    // (heed that I call the constructor of CLoggersInfra)
    auto callback3 = std::bind(&CLoggersInfra::NonStaticRedundencyManagerCallBack,
                               CLoggersInfra()); 
    std::cout << instance.Init(callback3) << "\n";
}
_

可能な出力:

_Hello from non member function!
Hello from static member callback!
Hello from non static member callback!
_

さらに _std::placeholders_ を使用すると、引数をコールバックに動的に渡すことができます(たとえば、fに文字列パラメーターがある場合、Initreturn f("MyString");を使用できます) 。

11
Roi Danton

Initはどの引数を取りますか?新しいエラーメッセージとは何ですか?

C++のメソッドポインターを使用するのは少し難しいです。メソッドポインター自体に加えて、インスタンスポインター(場合によってはthis)も提供する必要があります。たぶんInitはそれを別の引数として期待しているのでしょうか?

3
Konrad Rudolph

_m_cRedundencyManager_はメンバー関数を使用できますか?ほとんどのコールバックは、通常の関数または静的メンバー関数を使用するように設定されています。 このページ at C++ FAQ Liteで詳細を確認してください。

更新: 指定した関数宣言は、_m_cRedundencyManager_がvoid yourCallbackFunction(int, void *)という形式の関数を予期していることを示しています。したがって、この場合、メンバー関数はコールバックとして受け入れられません。静的メンバー関数 5月 動作しますが、それがあなたのケースで受け入れられない場合、次のコードも動作します。 _void *_からの不正なキャストを使用することに注意してください。

_
// in your CLoggersInfra constructor:
m_cRedundencyManager->Init(myRedundencyManagerCallBackHandler, this);
_
_
// in your CLoggersInfra header:
void myRedundencyManagerCallBackHandler(int i, void * CLoggersInfraPtr);
_
_
// in your CLoggersInfra source file:
void myRedundencyManagerCallBackHandler(int i, void * CLoggersInfraPtr)
{
    ((CLoggersInfra *)CLoggersInfraPtr)->RedundencyManagerCallBack(i);
}
_
3
e.James

クラスメンバー関数へのポインターは、関数へのポインターと同じではありません。クラスメンバは、暗黙の追加引数(thisポインタ)を受け取り、異なる呼び出し規約を使用します。

APIに非メンバーコールバック関数が必要な場合、それを渡す必要があります。

3
jalf

ネクロマンシング。
これまでの答えは少し不明瞭だと思います。

例を作りましょう:

ピクセルの配列(ARGB int8_t値の配列)があるとします

// A RGB image
int8_t* pixels = new int8_t[1024*768*4];

次に、PNGを生成します。そのためには、関数toJpegを呼び出します

bool ok = toJpeg(writeByte, pixels, width, height);

ここで、writeByteはコールバック関数です

void writeByte(unsigned char oneByte)
{
    fputc(oneByte, output);
}

ここでの問題:FILE *出力はグローバル変数でなければなりません。
マルチスレッド環境(httpサーバーなど)の場合は非常に悪いです。

そのため、コールバック署名を保持したまま、出力を非グローバル変数にする方法が必要です。

頭に浮かぶ直接的な解決策はクロージャーです。これは、メンバー関数を持つクラスを使用してエミュレートできます。

class BadIdea {
private:
    FILE* m_stream;
public:
    BadIdea(FILE* stream)  {
        this->m_stream = stream;
    }

    void writeByte(unsigned char oneByte){
            fputc(oneByte, this->m_stream);
    }

};

それから

FILE *fp = fopen(filename, "wb");
BadIdea* foobar = new BadIdea(fp);

bool ok = TooJpeg::writeJpeg(foobar->writeByte, image, width, height);
delete foobar;
fflush(fp);
fclose(fp);

ただし、期待に反して、これは機能しません。

その理由は、C++メンバー関数は、C#拡張関数のように実装されているためです。

だからあなたは

class/struct BadIdea
{
    FILE* m_stream;
}

そして

static class BadIdeaExtensions
{
    public static writeByte(this BadIdea instance, unsigned char oneByte)
    {
         fputc(oneByte, instance->m_stream);
    }

}

したがって、writeByteを呼び出すには、writeByteのアドレスだけでなく、BadIdeaインスタンスのアドレスも渡す必要があります。

したがって、writeByteプロシージャのtypedefがあり、次のようになっている場合

typedef void (*WRITE_ONE_BYTE)(unsigned char);

そして、あなたはこのように見えるwriteJpeg署名を持っています

bool writeJpeg(WRITE_ONE_BYTE output, uint8_t* pixels, uint32_t 
 width, uint32_t height))
    { ... }

2アドレスのメンバー関数を1アドレスの関数ポインターに(writeJpegを変更せずに)渡すことは基本的に不可能であり、回避する方法はありません。

C++で実行できる次善策は、ラムダ関数を使用することです。

FILE *fp = fopen(filename, "wb");
auto lambda = [fp](unsigned char oneByte) { fputc(oneByte, fp);  };
bool ok = TooJpeg::writeJpeg(lambda, image, width, height);

ただし、ラムダはインスタンスを隠しクラス( "BadIdea"クラスなど)に渡すことと何の違いもないため、writeJpegの署名を変更する必要があります。

手動クラスに対するラムダの利点は、1つのtypedefを変更するだけです

typedef void (*WRITE_ONE_BYTE)(unsigned char);

using WRITE_ONE_BYTE = std::function<void(unsigned char)>; 

そして、他のすべてをそのままにしておくことができます。

Std :: bindを使用することもできます

auto f = std::bind(&BadIdea::writeByte, &foobar);

しかし、これは舞台裏でラムダ関数を作成するだけであり、その場合はtypedefの変更も必要になります。

そのため、静的な関数ポインターを必要とするメソッドにメンバー関数を渡す方法はありません。

ただし、ソースを制御できれば、ラムダは簡単な方法です。
それ以外の場合は、運がありません。
C++でできることは何もありません。

注:
std :: functionには#include <functional>

ただし、C++ではCも使用できるため、依存関係のリンクを気にしない場合は、プレーンCで libffcall を使用してこれを行うことができます。

LibffcallをGNUからダウンロードします(少なくともubuntuでは、配布パッケージを使用しないでください-破損しています)、解凍します。

./configure
make
make install

gcc main.c -l:libffcall.a -o ma

main.c:

#include <callback.h>

// this is the closure function to be allocated 
void function (void* data, va_alist alist)
{
     int abc = va_arg_int(alist);

     printf("data: %08p\n", data); // hex 0x14 = 20
     printf("abc: %d\n", abc);

     // va_start_type(alist[, return_type]);
     // arg = va_arg_type(alist[, arg_type]);
     // va_return_type(alist[[, return_type], return_value]);

    // va_start_int(alist);
    // int r = 666;
    // va_return_int(alist, r);
}



int main(int argc, char* argv[])
{
    int in1 = 10;

    void * data = (void*) 20;
    void(*incrementer1)(int abc) = (void(*)()) alloc_callback(&function, data);
    // void(*incrementer1)() can have unlimited arguments, e.g. incrementer1(123,456);
    // void(*incrementer1)(int abc) starts to throw errors...
    incrementer1(123);
    // free_callback(callback);
    return EXIT_SUCCESS;
}

また、CMakeを使用する場合は、add_executableの後にリンカーライブラリを追加します

add_library(libffcall STATIC IMPORTED)
set_target_properties(libffcall PROPERTIES
        IMPORTED_LOCATION /usr/local/lib/libffcall.a)
target_link_libraries(BitmapLion libffcall)

または、libffcallを動的にリンクすることもできます

target_link_libraries(BitmapLion ffcall)

注意:
libffcallヘッダーとライブラリを含めるか、libffcallの内容を使用してcmakeプロジェクトを作成します。

2
Stefan Steiger

Initには次のオーバーライドがあることがわかります。

Init(CALLBACK_FUNC_EX callback_func, void * callback_parm)

ここで、CALLBACK_FUNC_EX

typedef void (*CALLBACK_FUNC_EX)(int, void *);
1
ofer

これは 質問と回答 から C++ FAQ Lite はあなたの質問と回答に関係する考慮事項を非常にうまくカバーしていると思います。リンクしたWebページ:

しないで。

メンバー関数は、呼び出すオブジェクトがなければ意味がないため、直接実行することはできません(X Window SystemがC++で書き直された場合、おそらく関数へのポインターだけでなく、周りのオブジェクトへの参照を渡します;自然にオブジェクト必要な機能を実現し、さらに多くの機能を実現します)。

0