第18回 解答例
RobotとControllerの相互参照を解く
演習の解答例。まず自分で書いてから読むこと。
まず壊れる形を書く
#pragma once
#include "Controller.hpp"
class Robot {
public:
Robot();
void update();
bool isObstacleAhead() const;
void stop();
private:
Controller controller;
bool obstacle;
};#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:前方宣言で解く
#pragma once
class Robot; // 前方宣言
class Controller {
public:
explicit Controller(Robot& robot);
void update();
private:
Robot& robot; // 参照だけ。所有はしない
};#include "Controller.hpp"
#include "Robot.hpp" // 中身を使うのはここから
Controller::Controller(Robot& robot)
: robot(robot) {
}
void Controller::update() {
if (robot.isObstacleAhead()) {
robot.stop();
}
}#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つだけだった。それなら引数で渡せばよい。
#pragma once
enum class Command {
GO,
STOP
};
class Controller {
public:
Command decide(bool obstacleAhead) const;
};#include "Controller.hpp"
Command Controller::decide(bool obstacleAhead) const {
if (obstacleAhead) {
return Command::STOP;
} else {
return Command::GO;
}
}#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ではどうなっているか
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を見ない。依存が一方向なので、そもそも前方宣言が出てこない。