第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]); // 10std::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::array | std::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 繋がっていたセンサだけ登録する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にした場合、どこが変わり、何ができなくなるかを説明する