Arduino で使う C++

Arduinoで使用している言語は,C ではなく C++ (C++11)です.つまり,クラス,constexpr,bool 型,string 型など相当幅広い機能が使用できるのです.たまに解説記事でも「C 言語ではこういう書き方をします」などと言われていますが,C++ においては正しくない事があります.C++ は C 言語に比べて遙かにたくさんの機能を持っているため,C 言語的な書き方は推奨されないことも多くあるのです.

さて,Arduino はプログラミングをあまりしない人がプロトタイピングに使うことも多くあります.わかる人には「C++ が使えるよ」と言えば一瞬ですが,そうではない人の方が多いのです.ここではそのような,プログラミング,特に C++ をよく知らない人向けにどのような機能が使えるのかを簡単に説明していきます.

クラス

当然 C++ ですから,クラスが使えます.継承もインターフェースも実装できます.

class Foo {
  int value = 0;
 
  public:
    void add(int addition) {
      value += addition;
    }
};
 
void setup() {
  Foo f;
  f.add(10);
}

また,同じフォルダにヘッダファイル (.h) とクラスファイル (.cpp) を追加すると,自動で読み込まれて使用できます.

Template

Templateは「違う型に対して一つのコードで記述する」ための機能です.Java や C# におけるジェネリクスという言い方が分かりやすいかもしれません.クラスを色々と使う場合には割とありがたさが出てきます.

// write して println する
template<class T, class U> void printAndWrite(T &obj, U value) {
  obj.write(value);
  Serial.println(value);
}
 
Servo servo;
servo.attach(5);
printAndWrite(servo, 10); // write が使えるなら何を渡してもよい
printAndWrite(Serial, "test"); // serial でもまったく同じコードが使用される

演算子のオーバーロード

プログラムの中の +<< のような計算に使用する記号を演算子と呼びます.そしてこの演算子は上書きすることができます.これを用いれば,例えばサーボモータを回転させるときに servo + 10 で10°動かすようにもできます.

便利なような,便利でないような.自分でいろいろクラスを書くときには便利になるのですが,やり過ぎると返ってわかりづらくなります.

// + の演算子をオーバーロードして書き換え
inline Servo &operator +(Servo &obj, T degree) {
  obj.write(degree);
  return obj;
}
 
Servo s;
 
void setup() {
  Foo f = Foo();
 
  s.attach(5);
  s.write(10); // 10°回転
  s + 10; // これでも10°回転できる
}

foreach

他の多くの言語と同様に,foreach構文が使えます.

int foo[] = {1,2,3,4,5};
// 普通の繰り返し
for (auto i = 0; i < sizeof(foo); i++) {
  Serial.println(i);
}
 
// foreach
for (auto i : foo) {
  Serial.println(i);
}

constexpr

constexpr は定数式です.引数の変数だけで返り値が決まるメソッドは,頭にconstexpr を付けると高速になったり副作用がないことを保証したりと色々利点があります.C++14では色々複雑に書けるのですが,C++11 なので return 文一つしか書けません.

constexpr int foo(int a, int b)
{
  return a < b ? a : b;
}
 
void setup() {
  foo(1, 3); // コンパイル時には単に 3 に置き換わるので高速
}

また,定数も constexpr で書けます.Ardino のコードサンプルでは#define が多用されていますが,こちらを使った方が安全でしょう.

constexpr string bar = "Hello.";
 
void setup() {
  constexpr int foo = 100;
  for (auto i = 0; i < foo; i++) { // ここでは foo が使える
    Serial.println(i);
  }
}
 
void loop() {
  // Serial.println(foo); // ここではfooが使えないのでコンパイルエラー
  Serial.println(bar); // これは使える
}

string

C++なので文字列が使えます.Serial.read()で1バイトずつ読み出すのではなく,Serial.readString()Serial.readStringUntil(char)を使えばstringで一発で取れます.

なお,readString は通信が完了したのか確認するために 1 秒間待ってしまうので,readStringUntilを使った方が高速です.

auto

autoが使えます.毎回型を調べて書く必要がなくなります.millis()longではなくintで受けてしまってバグが発生する,というミスもなくなります.

これは型推論なので,変なコードを書いたときはちゃんとエラーになります.JavaScriptのように,バグがあるのに走り続けるということがありません.

無名関数

ラムダ式が使えます.
auto func = [](int a, int b){return a * b;};
 
func(2, 3); // 3 が返る

これを利用すれば,割り込みなどの時に関数をいちいち書く必要がなくなります.

#include 
 
void foo() {
  digitalWrite(5, HIGH);
}
 
void setup() {
  MsTimer2::set(100, foo); // 関数をいちいち手前に書かなくてはいけないので面倒
  MsTimer2::set(100, []{digitalWrite(5, HIGH);}); // 一行で書ける
  MsTimer2::start();
 
  attachInterrupt(digitalPinToInterrupt(3), []{digitalWrite(5, HIGH);}, RISING);
}