第16回

STLとstd::vector

個数の管理を自分でやめ、標準ライブラリに任せる。

固定長配列で困ること

第13回では、センサを生の配列にまとめて回した。

Sensor* sensors[] = {&imu, &gps, &tof};
const int SENSOR_COUNT = sizeof(sensors) / sizeof(sensors[0]);

動くが、面倒が3つ残っている。

  • 要素数を自分で計算して持ち歩かなければならない
  • 実行中に増やせない(長さはコンパイル時に決まる)
  • 関数に渡すと長さの情報が落ちる
// 配列を渡すと、長さは別の引数で渡すしかない
void updateAll(Sensor* sensors[], int count);

渡す側と受け取る側でcountが食い違えば、範囲外アクセスになる。この「長さを人間が管理する」という仕事を手放すのが今回の目的である。

STLとは

C++の標準ライブラリのうち、データの入れ物(コンテナ)とそれを扱う仕組みをまとめてSTL(Standard Template Library)と呼ぶ。代表的なものは次のとおり。

名前何を入れるか
std::array個数が決まっている並び
std::vector個数が変わる並び
std::string文字列
std::mapキーと値の対応表

本講座で扱うのはstd::vectorとstd::arrayの2つだけである。どちらもテンプレートなので、<>の中に「何を入れるか」を書いて使う。

std::vector

#include <vector>

std::vector<int> values;

values.push_back(10);
values.push_back(20);

Serial.println(values.size());  // 2
Serial.println(values[0]);      // 10

std::vector<int>は「intを並べて入れる箱」という型である。push_back()で末尾に足すと、箱のほうが勝手に広がる。size()で今の個数が取れるので、SENSOR_COUNTのような変数を自分で用意する必要がなくなる。

最初から中身が決まっているなら、まとめて書ける。

std::vector<int> values = {10, 20, 30};

範囲for

for (int value : values) {
    Serial.println(value);
}

添字を書かないので、i <= size()のような境界の間違いが起きない。中身を書き換えたいときは参照で受ける。

for (int& value : values) {
    value = value * 2;
}

よく使う操作

書き方意味
push_back(x)末尾に1つ足す
size()今の個数
empty()空かどうか
clear()中身を全部捨てる
v[i]i番目。範囲は確認されない
at(i)i番目。範囲外なら例外を投げる
reserve(n)n個分の場所を先に確保する

vectorでセンサをまとめる

#include <vector>

#include "GPS.hpp"
#include "IMU.hpp"
#include "Sensor.hpp"
#include "ToF.hpp"

IMU imu;
GPS gps;
ToF tof(6);

std::vector<Sensor*> sensors = {&imu, &gps, &tof};

void loop() {
    for (Sensor* sensor : sensors) {
        sensor->update();
        sensor->print();
    }
}

SENSOR_COUNTが消え、loop()から数字がなくなった。センサを増やすときに触るのは初期化の1要素だけである。

生の配列                    std::vector
─────────                   ───────────
個数    自分で持つ          size() が答える
長さ    渡すと消える        一緒についてくる
増減    できない            push_back で増やせる

std::array

個数が最初から決まっているなら、std::vectorではなくstd::arrayを使う。

#include <array>

std::array<Sensor*, 3> sensors = {&imu, &gps, &tof};

for (Sensor* sensor : sensors) {
    sensor->update();
}

生の配列と同じ置き場所・同じ大きさのまま、size()と範囲forが使えるようになる。ヒープを使わない点がstd::vectorとの決定的な違いである。

生の配列・std::array・std::vector

int a[3]std::arraystd::vector
個数を実行中に変えられる××○
size()が使える×○○
関数に渡しても長さが残る×○○
ヒープを使う使わない使わない使う
追加の#include不要<array><vector>

マイコンで使うときの注意

std::vectorは、入りきらなくなるとより広い場所を確保し直し、中身をコピーして、元の場所を返す。これはヒープ(動的メモリ)の操作である。

push_back            push_back           push_back
    ↓                    ↓                   ↓
[10|  ]    →    [10|20]    →    [10|20|30|  ]
                                 ↑
                      ここで広い場所を確保し直してコピーする
  • 確保と解放を繰り返すとヒープが断片化し、やがて確保に失敗する
  • push_back()にかかる時間が一定でない(確保し直した回だけ重い)
  • RAMの少ないマイコンでは、失敗したときに打てる手が少ない

そこで、個数の上限が読めるならsetup()で先に場所を確保してしまう。

std::vector<Sensor*> sensors;

void setup() {
    sensors.reserve(8);  // 8個分の場所を先に確保する

    sensors.push_back(&imu);
    sensors.push_back(&gps);
    sensors.push_back(&tof);
}

reserve()しておけば、8個を超えるまで確保し直しは起きない。loop()の中では増やさない、という運用にすると挙動が読みやすくなる。

動かして確かめる

演習に入る前に、まずこのサンプルを動かして結果を見る。追加の部品は要らない。光るのはボードに載っている内蔵LEDで、それ以外はシリアルモニタの出力で確かめられる。code/examples/16-stl-vector/を開き、pio run -t uploadで書き込んでからpio device monitorで出力を見る(手順と、外付けのLEDなどをつなぐ場合は開発環境にまとめてある)。

code/examples/16-stl-vector/
├── include/
│   ├── Sensor.hpp
│   ├── IMU.hpp / GPS.hpp / ToF.hpp
└── src/
    ├── IMU.cpp / GPS.cpp / ToF.cpp
    └── main.cpp     繋がっていたセンサだけ登録する
code/examples/16-stl-vector/src/main.cpp(抜粋)
void setup() {
    Serial.begin(115200);
    delay(1000);

    // 確保をここで1回だけ済ませる。loop() では増やさない
    sensors.reserve(8);

    registerIfConnected(&imu, "IMU");
    registerIfConnected(&gps, "GPS");
    registerIfConnected(&tof, "ToF");

    Serial.print("sensors.size() = ");
    Serial.println(sensors.size());
}

void loop() {
    // 添字も境界条件も書かない
    for (Sensor* sensor : sensors) {
        sensor->update();
        sensor->print();
    }

    Serial.println();
    delay(1000);
}
シリアルモニタの出力
registered: IMU
not found : GPS
registered: ToF
sensors.size() = 2
IMU  roll=0.50 pitch=0.20
ToF  distance=375.00 mm

IMU  roll=1.00 pitch=0.40
ToF  distance=350.00 mm

確かめること。

  • GPSが繋がっていないことにしてあるので、sensors.size()が2になる。個数が実行時に決まっている
  • loop()に添字も個数も出てこない。SENSOR_COUNTが要らなくなった
  • isConnected()を書き換えて、3つとも登録されたときにsize()が3になることを確かめる
  • reserve(8)を消しても動く。何が変わるかを考える
  • この構成をstd::arrayで書けるかを考える(個数が実行時に決まる点に注意)

演習:センサをstd::vectorでまとめる

  • 第13回のSensor*の配列をstd::vector<Sensor*>に書き換える
  • 範囲forでupdate()とprint()を回す
  • setup()でreserve()してからpush_back()する形も書いてみる
  • このプログラムをstd::arrayにした場合、どこが変わり、何ができなくなるかを説明する