

コールバック関数を引数に取るライブラリの関数に、自作クラスのメンバー関数を渡したいのですがうまくいきません。  
どこが悪いのかご指摘をお願いいたします。

【やった事】  
① ラムダ関数で渡すとメンバ変数にアクセスできないので  
② `std::bind` でラップしてみたがコンパイルエラーになる。

[code]

    error: cannot convert 'std::_Bind<std::_Mem_fn<void (test_class::*)(unsigned char*, unsigned char)>(test_class*, std::_Placeholder<1>, std::_Placeholder<2>)>' to 'esp_now_send_cb_t {aka void (*)(unsigned char*, unsigned char)}' for argument '1' to 'int esp_now_register_send_cb(esp_now_send_cb_t)'
    
[/code]

【実現したい事】  
自作クラスのメンバー関数をコールバックとして渡したい。

【テストコード】  
espnow.h の中から抜粋

[code]

    typedef unsigned char       u8;
    typedef void (*esp_now_send_cb_t)(u8 *mac_addr, u8 status);
    int esp_now_register_send_cb(esp_now_send_cb_t cb);
    
[/code]

test_class.h

[code]

    class test_class
    {
    public:
        uint8_t val = 88;
        test_class() {};
        void initialize();
        void send_cb(uint8_t* macaddr, uint8_t status);
    };
    
[/code]

test_class.cpp

[code]

    void test_class::initialize()
    {
        Serial.println("initialize()");
    
        // esp_now_register_send_cb() へ send_cb() を渡したい。
    
        // ① このコードではコンパイルエラーは出ないが val にアクセスできない。
        esp_now_register_send_cb([](uint8_t* macaddr, uint8_t status) {
            Serial.print("esp_now_register_send_cb([]) : val = ");
            // Serial.println(val);     // コメント解除するとエラー
        });
    
        // ② std::bind を使って見たが esp_now_register_send_cb() へ渡せなかった。
        auto func = std::bind(&test_class::send_cb, this, std::placeholders::_1, std::placeholders::_2);
        uint8_t macaddr[] = { 0x09,0x00,0xA6,0x18,0x11,0xBC };
        func(macaddr, val);                 // 機能している
        esp_now_register_send_cb(func);     // コメント解除でコンパイルエラー
        // error: cannot convert 'std::_Bind<std::_Mem_fn<void (test_class::*)(unsigned char*, unsigned char)>(test_class*, std::_Placeholder<1>, std::_Placeholder<2>)>' to 'esp_now_send_cb_t {aka void (*)(unsigned char*, unsigned char)}' for argument '1' to 'int esp_now_register_send_cb(esp_now_send_cb_t)'
    }
    
    void test_class::send_cb(uint8_t* macaddr, uint8_t status) 
    {
        Serial.printf("send_cb()  : %d : %d\n", macaddr[0], status);
    }
    
[/code]

test.ino

[code]

    #include <arduino.h>
    #include <ESP8266WiFi.h>
    extern "C" {
      #include <espnow.h>
    }
    
    test_class test;
    void setup()
    {
        Serial.begin(115200);
        test.initialize();
    }
    
    void loop() {}
    
[/code]

【環境】  
WIndows 10  
Arduino IDE 1.8.3  
WSP8266 SDK 2.3.0

ご指摘、ご助言をよろしくお願いいたします。

【ご回答を頂いて】  
C++初心者として同じ初心者プログラマの方々のために残しておきたいと思います。  
この問題への解決にはC言語の「関数ポインタ」とC++言語の「メンバ関数ポインタ」の関数構造の違いを理解する必要がありました。

この違いについて初心者の私が説明するより(と、言うよりできない...)「c言語の関数ポインタとメンバ関数ポインタの違い」で検索すると詳細に説明又は応用解決に向けたサイトがたくさん出てきます。

初心者は問題解決に繋げる為のキーワードがわからない(又は連想できない)ので壁にぶつかって足踏みしてしまいます。  
そんな時、stackoverflowの皆さんのアドバイスで解決への方向が見いだせるのです。  
そんなstackoverflowで回答を寄せられている皆さんをいつも尊敬のまなざしで見ています。  
早く初心者を脱して他の皆さんのお役に立てるように精進したいと思います。（まだまだ遠い...）  
774RRさん、Hidekiさんありがとうございます。


CC BY-SA 3.0
2017-08-01T09:18:35.663
36847
2017-08-02T23:52:29.490
2017-08-02T23:52:29.490
19605
19605
question
c++ arduino
class メンバー関数をコールバックとして渡したい


残念ながら無理だと思います。

`std::bind`で作った関数は、普通の関数と呼び出し方は同じですが、内部構造が全然違うので、関数ポインタにすることができません。

ラムダ関数は、変数のキャプチャをしなければ普通の関数として使えます。しかし、メンバ変数にアクセスできるように this
をキャプチャしてしまうと、やはり普通の関数でなくなってしまうので、`esp_now_register_send_cb`に渡せなくなってしまいます。

一番いいのは、`esp_now_register_send_cb`を修正して

[code]

    int esp_now_register_send_cb(std::function<void(u8 *,u8)> cb);
    
[/code]

のようにすることですが、外部の関数だとすると、そもそも変更できないでしょう。

独立したコールバック関数を作り、そこでいったんコールバックを受け取り、そこから登録したオブジェクトのメンバ関数を呼び出すようにしたら、どうでしょうか。

* * *

参考までに this のキャプチャは

[code]

    [this](uint8_t* macaddr, uint8_t status) {
            Serial.print("esp_now_register_send_cb([]) : val = ");
            Serial.println(this->val);
        }
    
[/code]

のようにできます。


CC BY-SA 3.0
2017-08-01T18:58:35.810
36857
2017-08-01T18:58:35.810
3605
36847
answer


昔懐かしき MFC では下記のような手続きを採用していましたが `esp_now_register_send_cb`
が変更できないのだとこの手法は取れません。まあ参考までに。

非静的メンバ関数は暗黙の引数 `this` を取る関係で [c](/questions/tagged/c "'c' のタグが付いた質問を表示")
互換な通常関数（非メンバ関数）とはアセンブラレベルで呼び出し手続きが非互換です。静的メンバ関数は `this`
を取らないので通常関数と呼び出し手続きが互換ということになります。

> `typedef void (*esp_now_send_cb_t)(u8 *mac_addr, u8 status);`

は [c](/questions/tagged/c "'c' のタグが付いた質問を表示")
の通常関数へのポインタ型なので、これでクラスの非静的メンバ関数を指し示すことはできません。

クラスの非静的メンバ関数をコールバックさせたいのであれば、

  * 静的メンバ関数は [c](/questions/tagged/c "'c' のタグが付いた質問を表示") の通常関数と互換であるから、これを通常関数へのポインタにて指し示すことができる (が `this` は無い)
  * 非静的メンバ関数を呼ぶには `this` が必要

ということで `this` を別途引き渡す必要があり、実装はこんな感じ

[code]

    class test_class {
        static int CallbackSugarFunc(void* arg) {
            return reinterpret_cast<test_class*>(arg)->callbackfunc();
        }
        int callbackfunc();
    };
    
[/code]

コールバックを登録するライブラリ側も

[code]

    typedef int (*callback_entry_func)(void* arg);
    int register_callback(callback_entry_func f, void* arg_to_callbackfunc);
    
[/code]

のように、追加の `void*` を渡せるようになっているので、登録する際に `this` を渡せばよくて

[code]

    void test_class::initialize() {
        register_callback(&test_class::CallbackSugarFunc, this);
    }
    
[/code]

追加の引数として `this` を受け渡す方法がライブラリ側に無い場合には、コールバック一覧を管理するための別クラスを作って、その中で callback
対象とする `this` のリストを保持しておく必要がありますね。

[c#](/questions/tagged/c%23 "'c#' のタグが付いた質問を表示") の `delegate` は `this`
と非静的メンバ関数へのポインタをセットで管理してくれてとても楽。さすがは後発言語と言うところでしょう。
[c++](/questions/tagged/c%2b%2b "'c++' のタグが付いた質問を表示") の足らないところを補完してくれています。


CC BY-SA 3.0
2017-08-02T00:13:40.947
36860
2017-08-02T00:13:40.947
8589
36847
answer