2021年11月7日日曜日

cppでクラスメンバー関数を引数として受け取る関数の書き方


背景

以前ROSのsubscriberにクラスメンバー関数を渡したことがあり、クラスメンバー関数を引数に渡す関数が定義可能なのは把握していました。
それを使うと便利な場面があったので、書き方を調べて実現する機会がありました。
今後も使う場面がありそうなので、備忘録を兼ねて書き方を記事に残します。

使ったもの

Arduinoのビルド環境
Arduino IDEやPlatformIOのArduino環境で同じことができます。
cpp11と同等の機能が使えます。

クラスは関係なく、グローバルな関数やラムダ式を受け取る関数の書き方

「Streamのポインタを引数として扱う関数」を受け取る関数はこのように書けます。
void handlePrint(void (*fnToPrint)(Stream* serial)) {
fnToPrint(&Serial);
}

「Streamのポインタを引数として扱う関数」を定義して渡したり、「Streamのポインタを引数として扱うラムダ式」を渡して実行できます。
void aFnToPrint(Stream* serial) {
serial->println("print in a fn");
}

void setup() {
Serial.begin(115200);
}

void loop() {
handlePrint(aFnToPrint);
handlePrint([](Stream* serial){
serial->println("print in a lambda");
});
delay(2000);
}

Arduinoで実行してシリアルモニタを確認すると下記のログが確認できます。
print in a fn
print in a lambda

参考:
ArduinoのコードだとWireのonReceiveなどに関数を渡す実装が参考になります。
Wire/src/Wire.cpp#L364

特定のクラスのメンバー関数を受け取れる関数の書き方

下記の「Bookというクラスの「Streamのポインタを引数として受け取るメンバー関数(下記の例ではprintTitle)」」を引数として受け取る関数を定義します。
class Book {
public:
Book(String title) {
this->title = title;
}

void printTitle(Stream* serial) {
serial->println("title is " + title);
}

private:
String title;
};

書き方としては「Bookクラスの関数呼び出し」と「対象となるインスタンスのポインタ」を引数として受け取り、2つを組み合わせてクラスメンバー関数を実行します。
void handlePrint(void (Book::*fnToPrint)(Stream* serial), Book* book) {
(book->*fnToPrint)(&Serial);
}

「クラスメンバー関数printTitleのポインタ(&Book::printTitle)」と「Bookのインスタンス」をhandlePrintに渡すことで、関数が実行されます。
Book bookPysics("physics");
Book bookMath("math");

void setup() {
Serial.begin(115200);
}

void loop() {
handlePrint(&Book::printTitle, &bookPysics);
handlePrint(&Book::printTitle, &bookMath);
delay(2000);
}

Arduinoで実行してシリアルモニタを確認すると下記のログが確認できます。
title is physics
title is math

参考:
How can I pass a member function where a free function is expected?

任意のクラスのメンバー関数を受け取れる関数の書き方

templateを利用すると任意のクラスのメンバー関数を受け取れる関数を定義できます。
template<typename ObjT = void>
void handlePrint(void (ObjT::*fnToPrint)(Stream* serial), ObjT* instance) {
(instance->*fnToPrint)(&Serial);
}

先ほどの例で定義したBookに加えてAnimalというクラスも定義し、BookとAnimalそれぞれのインスタンスに対してhandlePrintを通して関数を呼び出します。
class Animal {
public:
Animal(String kind) {
this->kind = kind;
}

void printKind(Stream* serial) {
serial->println("kind is " + kind);
}

private:
String kind;
};

Book bookPysics("physics");
Book bookMath("math");
Animal animalGorilla("gorilla");
Animal animalLion("lion");

void setup() {
Serial.begin(115200);
}

void loop() {
handlePrint(&Book::printTitle, &bookPysics);
handlePrint(&Book::printTitle, &bookMath);
handlePrint(&Animal::printKind, &animalGorilla);
handlePrint(&Animal::printKind, &animalLion);
delay(2000);
}

Arduinoで実行してシリアルモニタを確認すると下記のログが確認できます。
title is physics
title is math
kind is gorilla
kind is lion

参考:
rosserialのsubscriberの実装を参考にしました。
rosserial_client/src/ros_lib/ros/subscriber.h#L59

余談: ラムダ式はcaptureを利用するとポインタを使えなくなる

「captureを設定して中でクラスメンバー関数を呼び出せるようにしたラムダ式」をクラスメンバー関数で定義した場合、「captureを設定したラムダ式はポインタを利用できない」という制約により、引数として渡せなくなるようです。

具体的には、下記のようにprintAll関数内のcaptureを利用したラムダ式はhandlePrint関数の引数として渡せません。
void handlePrint(void (*fnToPrint)(Stream* serial)) {
fnToPrint(&Serial);
}

class Animal {
public:
Animal(String kind) {
this->kind = kind;
}

void printKind(Stream* serial) {
serial->println("kind is " + kind);
}

void printAll() {
handlePrint([this](Stream* serial) { printKind(serial); }); // Error here
}

private:
String kind;
};

VSCode上では下記のように赤線が出て、マウスカーソルを合わせると型が合ってないと説明が出ます。


構わずビルドすると、下記のエラーが発生します。
src/main.cpp: In member function 'void Animal::printAll()':
src/main.cpp:42:64: error: no matching function for call to 'handlePrint(Animal::printAll()::<lambda(Stream*)>)'
handlePrint([this](Stream* serial) { printWeight(serial); });
^
src/main.cpp:17:6: note: candidate: void handlePrint(void (*)(Stream*))
void handlePrint(void (*fnToPrint)(Stream* serial)) {
^~~~~~~~~~~
src/main.cpp:17:6: note: no known conversion for argument 1 from 'Animal::printAll()::<lambda(Stream*)>' to 'void (*)(Stream*)'

クラスメンバー関数内で定義したcapture付きのlambda式は「Animal::printAll()::<lambda(Stream*)>」という型になるのですが、それのポインタを関数の引数として扱う方法が現在のcppでは無いようです。

「仕様が変わってこう書けばできるようになった」や「cpp11でもこう書けば実現できる」など分かる方がいらっしゃいましたら、コメントなどで共有していただけると嬉しいです。

参考:
Passing capturing lambda as function pointer
ラムダ・キャプチャーの理解

まとめ

templateの利用により任意のクラスのクラスメンバー関数を引数として扱える関数を定義できました。

0 件のコメント :