第12回

純粋仮想関数と抽象クラス

「必ず実装させる」仕組みとしてのインターフェース。

純粋仮想関数

class Sensor {
public:
    virtual void update() = 0;
};

= 0は「ここには実装がない。派生クラスが必ず実装すること」という意味である。

抽象クラス

Sensorそのものを直接オブジェクトとして扱わないことがある。そのとき、オブジェクトとして作れないようにする。具体的な実装は派生クラスで行う。

Sensor sensor; // エラー

派生クラスで実装する。

class IMU : public Sensor {
public:
    void update() override;
};

インターフェースとして利用する

Sensor
  │
  ├── IMU
  ├── GPS
  └── ToF

共通して、

update()

を持つことを要求できる。新しいセンサを追加する人は、Sensorを継承した時点でupdate()の実装を強制される。

動かして確かめる

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

code/examples/12-abstract/
├── include/
│   ├── Sensor.hpp     純粋仮想関数だけ(= 0)。.cpp を持たない
│   ├── IMU.hpp
│   └── Barometer.hpp  あとから足したセンサ
└── src/
    ├── IMU.cpp
    ├── Barometer.cpp
    └── main.cpp
code/examples/12-abstract/include/Sensor.hpp
// 第12回:共通の「約束ごと」だけを決めるクラス

#pragma once

class Sensor {
public:
    virtual ~Sensor() = default;

    /**
     * @brief センサの値を読み直す
     * @note = 0 なので、ここに実装はない。継承した側が必ず実装する。
     */
    virtual void update() = 0;

    /**
     * @brief 現在の値をシリアルに表示する
     */
    virtual void print() const = 0;
};
code/examples/12-abstract/src/main.cpp
// 第12回:抽象クラスは「約束ごと」。オブジェクトとしては作れない。
//
// 下の Sensor sensor; のコメントを外すとビルドが失敗する。
// エラーメッセージを一度自分の目で見てから、また戻すとよい。

#include <Arduino.h>

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

IMU imu;
Barometer baro;

// Sensor sensor;
//   → error: cannot declare variable 'sensor' to be of abstract type 'Sensor'
//     純粋仮想関数が残っているので、実体は作れない

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

    Serial.println("Sensorを継承した2つは、どちらも update() と print() を持っている");
}

void loop() {
    // どちらも「Sensorの約束ごと」を満たしているので、同じ順番で呼べる
    imu.update();
    imu.print();

    baro.update();
    baro.print();

    Serial.println();
    delay(1000);
}
シリアルモニタの出力
Sensorを継承した2つは、どちらも update() と print() を持っている
IMU  roll=0.50 pitch=0.20
Baro pressure=1012.50 hPa

IMU  roll=1.00 pitch=0.40
Baro pressure=1012.00 hPa

確かめること。

  • // Sensor sensor;のコメントを外すとビルドが止まり、cannot declare variable 'sensor' to be of abstract type 'Sensor'と出る
  • Barometer.cppからprint()の実装を消すと、Barometer baro;の行でエラーになる。実装を忘れられない
  • Sensor.hppに対応するSensor.cppがない。実装が1つもないので要らない
  • IMUとBarometerは中身が全く違うのに、呼び出し方は同じになっている

演習

  • 第11回のSensor::update()を純粋仮想関数に変える
  • Sensor sensor;と書くとどうなるかを実際に確かめる
  • 新しくToFクラスを追加し、update()を実装する