第16回 解答例

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

演習の解答例。まず自分で書いてから読むこと。

解答

src/main.cpp
#include <Arduino.h>

#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;

void setup() {
    Serial.begin(115200);

    sensors.reserve(8);  // 8個分まで確保し直しが起きない

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

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

    delay(1000);
}

第13回との差

消えたのは次の1行である。

const int SENSOR_COUNT = sizeof(sensors) / sizeof(sensors[0]);  // 不要になった

同時にloop()からも添字と境界の判定が消えた。

// 第13回
for (int i = 0; i < SENSOR_COUNT; i++) {
    sensors[i]->update();
}

// 第16回
for (Sensor* sensor : sensors) {
    sensor->update();
}

個数を人間が管理しなくなったので、個数の食い違いという種類のバグが消えた。これが今回いちばん大きい変化である。

初期化子リストで書く形

センサが最初から全部そろっているなら、setup()を使わずに書いてもよい。

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

この形は短いが、確保がグローバル変数の初期化の時点で起きる。setup()の中に寄せておくと、確保の順番とタイミングが追いやすい。

reserveを入れた理由

reserve()なしで3回push_back()すると、処理系によっては確保が複数回起きる。

reserve なし
  push_back → 1個分 確保
  push_back → 2個分 確保し直してコピー、1個分を解放
  push_back → 4個分 確保し直してコピー、2個分を解放
                    ↑ 使われない隙間がヒープに残る

reserve(8) あり
  push_back → 確保済みの場所に置くだけ
  push_back → 置くだけ
  push_back → 置くだけ

3個では実害は出ないが、「確保はsetupで1回だけ」という形を習慣にしておくと、センサが増えたときにそのまま通用する。

std::arrayにした場合

#include <array>

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

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

loop()は1文字も変わらない。変わるのは宣言と、できなくなることである。

std::vectorstd::array
個数の指定書かない<Sensor*, 3>と書く
push_back()使える無い
reserve()必要不要(確保が起きない)
ヒープ使う使わない
範囲for・size()使える使える

つまりセンサの構成がコンパイル時に決まっているなら、この演習の正解はstd::arrayである。std::vectorが要るのは、たとえば「接続されているセンサを起動時に探索し、見つかったものだけ登録する」ように個数が実行時に決まる場合である。

void setup() {
    sensors.reserve(8);

    if (imu.begin()) {
        sensors.push_back(&imu);   // 繋がっていたセンサだけ登録する
    }
    if (gps.begin()) {
        sensors.push_back(&gps);
    }
}

よくある間違い

// 良くない例1:vector の中身を値にしてしまう
std::vector<Sensor> sensors;   // Sensorは抽象クラスなので、そもそも入れられない
// 良くない例2:size() と int を比べて警告が出る
for (int i = 0; i < sensors.size(); i++) {   // size() は符号なし
}

// 範囲forにすれば、この比較そのものが不要になる
for (Sensor* sensor : sensors) {
}
// 良くない例3:loop() の中で毎回作り直す
void loop() {
    std::vector<Sensor*> sensors = {&imu, &gps, &tof};  // 毎周 確保と解放
}

3番目は動くが、ヒープの確保と解放を毎周繰り返すため、断片化の原因になる。std::vectorは寿命の長い場所に置く。

→ 第16回の本文に戻る