前回 に続き、Arduinoの小ネタです。
Arduinoでコールバック関数を書くとき、予め関数を宣言してから、コールバック登録関数にその関数を代入するのが普通でした。
uint32_t callback(uint8_t arg){
return arg * 2;
}
app.setCallback(callback);
しかし、ArduinoはC++11の新機能であるラムダ式が使えます。
ラムダ式を使うことで、JavaScriptのようにコールバック関数を無名関数として登録することができます。
app.addEventListener("load", ()=>{
// hogehoge
});
書き方
app.setCallback([](uint8_t arg){
return (uint32_t)(arg * 2);
});
このようになります。
最初の[]
はキャプチャと言い、外側にある変数をラムダ式の中へ取り込むことができます。
しかし、凝ったことをしない限りは[]
で何もキャプチャしないような使い方になると思います。
uint8_t num = 0;
// num を "コピー"
auto lambda1 = [num](){
Serial.println(num);
}
// num を "参照"
auto lambda2 = [&num](){
Serial.println(num);
}
次の(){}
は、普通の関数と同じく、引数と中身になります。
ただし注意点としては、ラムダ式の返り値はreturn
値から推論されます。
なので、コールバックの返り値が数値型など複数の型となりえる場合は、返したい型へ明示的にキャストしてあげると良いです。
// app.setCallbackの関数の返り値は uint32_t とする
app.setCallback([](uint8_t arg){
// 返り値は暗示的に uint8_t と推論される => NG
return arg++;
// 返したい型 uint32_t へ明示的にキャストする
return (uint32_t)arg++;
});
最後に、コールバック関数の場合は必要ありませんが、末尾に()
を付けることでそのラムダ式を即時実行できます。
auto lambda = [](uint8_t arg){
return arg;
}(0xFF);
// 末尾に () を付ける
// 引数も代入できる
利点
ラムダ式を使用する利点としては、名前空間の汚染を抑えることができます。
即時実行関数としても使えることから、変数スコープを制限するためのクロージャとしても強力です。
例えば、Arduinoではsetup()
関数の中でコールバック登録などの各種初期化をすると思います。
そして、通常であればコールバック関数はグローバル空間で宣言します。
MyApp1 app1;
MyApp2 app2;
volatile uint32_t foo = 0x12345678;
void callback1(){
Serial.println(foo);
}
void callback2(){
Serial.println(foo);
}
void setup(){
app1.setCallback(callback1);
app2.setCallback(callback2);
}
変数foo
は、スケッチの至るところから変更される可能性がある想定です。
この時点で関数callback1
とcallback2
がグローバル空間を汚染しています。
これを、ラムダ式を使用した書き方にすると、以下のようになります。
MyApp1 app1;
MyApp2 app2;
volatile uint32_t foo = 0x12345678;
void setup(){
app1.setCallback([](){
Serial.println(foo);
});
app2.setCallback([](){
Serial.println(foo);
});
}
おまけ
グローバル空間の汚染を防ぐ手段として、名前空間namespace
を利用するのも有効な手です。
例えば、グローバル空間に宣言しなければならない変数は、このように名前空間としてひと纏まりにすると、分かりやすくなります。
namespace Config{
volatile bool foo = false;
volatile uint8_t bar = 0x00;
}
// 名前空間名::変数 で使える
void setup(){
Serial.println(Config::foo);
}
またnamespace
に名前を付けないで使用する "無名名前空間" と手法があります。
これは、ソースファイルが複数に跨っている場合、ソースファイル単位でスコープを限定するのにとても役立ちます。
// 名前を付けない
// このソースファイルのみで使え、他のソースファイルからは参照できない
namespace{
volatile bool foo = false;
volatile uint8_t bar = 0x00;
}
void setup(){
// そのまま使える
Serial.println(foo);
}