LoginSignup
1
1

More than 3 years have passed since last update.

Arduino(C++11)でコールバック関数を無名関数として書く(名前空間の汚染について)

Last updated at Posted at 2020-07-15

前回 に続き、Arduinoの小ネタです。

Arduinoでコールバック関数を書くとき、予め関数を宣言してから、コールバック登録関数にその関数を代入するのが普通でした。

Arduino
uint32_t callback(uint8_t arg){
    return arg * 2;
}

app.setCallback(callback);

しかし、ArduinoはC++11の新機能であるラムダ式が使えます。

ラムダ式を使うことで、JavaScriptのようにコールバック関数を無名関数として登録することができます。

JavaScript
app.addEventListener("load", ()=>{
    // hogehoge
});

書き方

Arduino
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++;
});

最後に、コールバック関数の場合は必要ありませんが、末尾に()を付けることでそのラムダ式を即時実行できます。

IIFE
auto lambda = [](uint8_t arg){
    return arg;
}(0xFF);
// 末尾に () を付ける
// 引数も代入できる

利点

ラムダ式を使用する利点としては、名前空間の汚染を抑えることができます。
即時実行関数としても使えることから、変数スコープを制限するためのクロージャとしても強力です。

例えば、Arduinoではsetup()関数の中でコールバック登録などの各種初期化をすると思います。
そして、通常であればコールバック関数はグローバル空間で宣言します。

例1
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は、スケッチの至るところから変更される可能性がある想定です。

この時点で関数callback1callback2がグローバル空間を汚染しています。
これを、ラムダ式を使用した書き方にすると、以下のようになります。

例2
MyApp1 app1;
MyApp2 app2;

volatile uint32_t foo = 0x12345678;

void setup(){
    app1.setCallback([](){
        Serial.println(foo);
    });

    app2.setCallback([](){
        Serial.println(foo);
    });
}

おまけ

グローバル空間の汚染を防ぐ手段として、名前空間namespaceを利用するのも有効な手です。

例えば、グローバル空間に宣言しなければならない変数は、このように名前空間としてひと纏まりにすると、分かりやすくなります。

Arduino
namespace Config{
    volatile bool foo = false;
    volatile uint8_t bar = 0x00;
}

// 名前空間名::変数 で使える
void setup(){
    Serial.println(Config::foo);
}

またnamespaceに名前を付けないで使用する "無名名前空間" と手法があります。
これは、ソースファイルが複数に跨っている場合、ソースファイル単位でスコープを限定するのにとても役立ちます。

Arduino
// 名前を付けない
// このソースファイルのみで使え、他のソースファイルからは参照できない
namespace{
    volatile bool foo = false;
    volatile uint8_t bar = 0x00;
}

void setup(){
    // そのまま使える
    Serial.println(foo);
}
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1