第17回 解答例

センサの所有をunique_ptrに任せる

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

まず生ポインタで書く

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

#include "IMU.hpp"
#include "Sensor.hpp"

void readOnce() {
    Sensor* sensor = new IMU();

    sensor->update();
    sensor->print();

    delete sensor;
}

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

void loop() {
    readOnce();

    Serial.print("free heap: ");
    Serial.println(ESP.getFreeHeap());

    delay(1000);
}

deleteを消すと何が起きるか

delete sensor;の行をコメントアウトして動かすと、ESP.getFreeHeap()の値が1秒ごとに減り続ける。

free heap: 294512
free heap: 294488     ← IMU1個分ずつ減っていく
free heap: 294464
free heap: 294440
   ...
やがて確保に失敗して異常終了する

エラーも警告も出ない。プログラムは正常に動いているように見えたまま、時間をかけて死ぬ。これがリークのいちばん厄介なところである。

unique_ptrに書き換える

#include <memory>

void readOnce() {
    std::unique_ptr<Sensor> sensor = std::make_unique<IMU>();

    sensor->update();
    sensor->print();

}  // ここで解放される

変わったのは宣言の1行で、deleteは消えた。getFreeHeap()は一定の値に戻る。

生ポインタunique_ptr
解放の記述deleteを自分で書く書かない
途中でreturnした場合解放されない解放される
二重解放起こせるコピーがコンパイルエラーになる
ポインタの大きさ4バイト(ESP32-C5は32ビット)同じ

vectorにまとめる

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

#include <memory>
#include <vector>

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

std::vector<std::unique_ptr<Sensor>> sensors;

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

    sensors.reserve(8);

    sensors.push_back(std::make_unique<IMU>());
    sensors.push_back(std::make_unique<GPS>());
    sensors.push_back(std::make_unique<ToF>(6));
}

void loop() {
    for (const std::unique_ptr<Sensor>& sensor : sensors) {
        sensor->update();
        sensor->print();
    }

    delay(1000);
}

deleteは1行もない。sensorsが壊れるとき、持っている3つがすべて解放される。

shared_ptrは必要か

不要である。理由は、このプログラムでsensors以外にセンサを持つものが存在しないからである。

問いこの演習での答え
持ち主は何人いるか1人(sensorsだけ)
どれが最後に手を離すか決まるか決まる(sensorsが消えるとき)
生死を確認しながら使う相手がいるかいない

3つすべてが「はい」でないかぎりunique_ptrで足りる。shared_ptrにするとカウント用の領域が増え、コピーのたびに増減の処理が走るだけで、得るものがない。

shared_ptrが要るのは、たとえば「同じIMUを、姿勢制御と記録の2つが別々の寿命で参照する」ような場合である。その場合は第18回の循環参照にも気を配ることになる。

マイコンでの本当の答え

ここまでスマートポインタで書いてきたが、この演習の構成ではnew自体が不要である。

#include <array>

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

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

void loop() {
    for (Sensor* sensor : sensors) {
        sensor->update();
        sensor->print();
    }
}
書き方ヒープ使うべき場面
静的に置いてSensor*使わない種類と個数がコンパイル時に決まる。既定の選択
unique_ptr+vector使う構成が実行時に決まる(接続されたセンサだけ使う等)
shared_ptr使う持ち主が複数で、寿命を予測できない

→ 第17回の本文に戻る