第18回 解答例

RobotとControllerの相互参照を解く

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

まず壊れる形を書く

include/Robot.hpp
#pragma once

#include "Controller.hpp"

class Robot {
public:
    Robot();

    void update();
    bool isObstacleAhead() const;
    void stop();

private:
    Controller controller;
    bool obstacle;
};
include/Controller.hpp(これが通らない)
#pragma once

#include "Robot.hpp"

class Controller {
public:
    explicit Controller(Robot& robot);

    void update();

private:
    Robot& robot;
};

エラーの読み方

include/Controller.hpp:7:26: error: 'Robot' has not been declared
     explicit Controller(Robot& robot);
                         ^~~~~
include/Controller.hpp:12:5: error: 'Robot' does not name a type
     Robot& robot;
     ^~~~~

読むべき点は2つある。

  • エラーが出たのはController.hppだが、原因はRobot.hpp側のincludeにある
  • #include "Robot.hpp"は確かに書いてあるのに「宣言されていない」と言われている

2点目が循環includeの特徴である。#pragma onceのために2回目の読み込みが丸ごと飛ばされ、class Robotの定義に到達する前にControllerの定義が始まっている。

main.cpp
  └─ #include "Robot.hpp"
       ├─ #pragma once      ← Robot.hpp を読んだ印が付く
       ├─ #include "Controller.hpp"
       │    ├─ #pragma once
       │    ├─ #include "Robot.hpp"  ← 印があるので飛ばされる(空)
       │    └─ class Controller { Robot& robot; }   ← Robot が無い!
       └─ class Robot { ... }                        ← ここで定義される(遅い)

解答1:前方宣言で解く

include/Controller.hpp
#pragma once

class Robot;  // 前方宣言

class Controller {
public:
    explicit Controller(Robot& robot);

    void update();

private:
    Robot& robot;  // 参照だけ。所有はしない
};
src/Controller.cpp
#include "Controller.hpp"

#include "Robot.hpp"  // 中身を使うのはここから

Controller::Controller(Robot& robot)
    : robot(robot) {
}

void Controller::update() {
    if (robot.isObstacleAhead()) {
        robot.stop();
    }
}
src/Robot.cpp
#include "Robot.hpp"

Robot::Robot()
    : controller(*this), obstacle(false) {
}

void Robot::update() {
    controller.update();
}

bool Robot::isObstacleAhead() const {
    return obstacle;
}

void Robot::stop() {
    // モータを止める
}

Robot.hppは1文字も変えていない。直したのはController.hppの#includeを前方宣言に置き換えた1行だけである。

初期化順の落とし穴

// 良くない例:コンストラクタの中で相手を使う
Controller::Controller(Robot& robot)
    : robot(robot) {
    if (robot.isObstacleAhead()) {  // Robotのメンバはまだ初期化されていない
        // ...
    }
}

Robot::Robot()の初期化リストは上から順に動くので、controller(*this)が走る時点でobstacleはまだ初期化されていない。読んだ値は不定である。

Robot::Robot() の初期化リスト
  controller(*this)   ← Controllerのコンストラクタが動く
       │                  この中で robot.isObstacleAhead() を呼ぶと
       │                  obstacle はまだ未初期化
  obstacle(false)     ← 初期化されるのはこの後

コンストラクタでは受け取った参照を保存するだけにし、相手を使うのはupdate()からにする。

解答2:相互参照そのものを消す

ControllerがRobotから取っていたのは「障害物があるか」の1つだけだった。それなら引数で渡せばよい。

include/Controller.hpp
#pragma once

enum class Command {
    GO,
    STOP
};

class Controller {
public:
    Command decide(bool obstacleAhead) const;
};
src/Controller.cpp
#include "Controller.hpp"

Command Controller::decide(bool obstacleAhead) const {
    if (obstacleAhead) {
        return Command::STOP;
    } else {
        return Command::GO;
    }
}
src/Robot.cpp
#include "Robot.hpp"

void Robot::update() {
    Command command = controller.decide(obstacle);

    if (command == Command::STOP) {
        stop();
    }
}

Controller.hppからRobotという名前が完全に消えた。前方宣言も要らない。

解答1(前方宣言)解答2(引数で渡す)
依存の向き双方向一方向(Robot→Controller)
Controller単体でテストRobotを用意しないとできないdecide(true)を呼ぶだけ
初期化順の問題気を配る必要がある起きない
欲しい値が増えたときそのまま呼べる引数が増える

引数が3つ4つに増えてくると解答2は窮屈になる。そのときは値をまとめた構造体を渡すか、本文の案2(抽象クラスで向きを変える)に進む。最初から解答1に飛ばず、解答2で足りないかを先に確かめるのが手順である。

final-projectではどうなっているか

code/final-project/include/Controller.hpp
class Controller {
public:
    Controller(Motor& motor, DistanceSensor& distance);

    void update();
    RobotState getState() const;

private:
    Motor& motor;
    DistanceSensor& distance;
    RobotState state;
};

ControllerはRobotを知らず、必要な部品(MotorとDistanceSensor)だけを参照で受け取っている。RobotがControllerを持ち、ControllerはRobotを見ない。依存が一方向なので、そもそも前方宣言が出てこない。

→ 第18回の本文に戻る