ストラウストラップのプログラミング入門:組み込みシステムプログラミング

ストラウストラップのプログラミング入門を買って読んでる。
25章の「組み込みシステムプログラミング」で C++ で組み込みシステムを実装するときの注意点がまとまっていて、どうしても読みたかったので買ったのだけど…1000ページ超えってぶ厚すぎ!
電子書籍で出して欲しかったなぁ。

気になったところをまとめてみる。

予測可能性

予測可能性は「ある処理にかかる時間が一定であるかどうか」を指す。
ハードリアルタイムを実現するためにはこれが大事。
例えば、要素数が分からないリストの線形探索にかかる時間は予測可能ではない。
ハードリアルタイムに対応する為に避けなければいけない実装として下記が挙げられている。

  • new と delete を使ったフリーストア割り当て
  • 例外
  • dynamic_cast
メモリ管理(new と delete を使ったフリーストア割り当て)

プログラムの実行時に問題となるメモリは「スタックメモリ」と「ヒープメモリ」。

  • 「スタックメモリ」はプログラムの関数呼び出しのネストが許容範囲内に収まるようにすることが必要。この関係上再帰呼び出しを禁止にする、もしくは再帰の代わりに反復(反復は末尾再帰と同義??)を使用するといった対処が必要。
  • new が予測可能でないのは、メモリの断片化が発生するため。断片化が発生するとちょうどよいサイズのメモリを探す時間が長くなるため new にかかる時間が長くなる。
    • 対策としては、 スタックを使用する、オブジェクトをプールするという方法がある。
    • スタックだとサイズが異なるオブジェクトの割り当てができる。プールだとできない。
    • STL のコンテナ、string は間接的に new を使用するため使用しない。
例外

各 throw に対してどの catch が呼び出されるか、throuw がそこに到達するまでにどれくらいかかるかが、予測可能性に関わる。

dynamic_cast

根本的な問題ではないとだけ書かれてある。なんのこっちゃ?

アドレス、ポインタ、配列

組み込みシステムは信頼性が重要。そのために、ポインタの取り扱いに注意することとして挙げられている

チェックされない変換

明示的な変換は使用しないこと。
ただし、下記のようにデバイスドライバへのアクセスなどでやむを得ない場合はある。

Device_driver* p = reinterpret_cast<Device_driver*>(0xffb8);

※ reinterpret_cast は関連のない型の間の変換を行う。

正しく機能しないインターフェイス

Shape の子クラス Circle が定義されているときに下記の実装は問題を起こす

void poor (Shape* p, int sz) {  // Shape* p は Shape の配列を意図している
  for (int i = 0; i < sz; ++i) p[i].draw();
}
void main() {
  vector<Circle> circles;
  …circles にいくつかデータを詰める処理が入ったとして… 
  poor(&circles[0], circles.size());
}

Circle は子クラスなので Shape よりもメモリを多く使用する。
しかし、poor() 内の for 文のポインタ p は sizeof(Share) ずつずれて行くため、プログラムがクラッシュする
(p の実体が Circle だと sizeof(Circle) になっていない上手くいかない)
実は、Circle で属性が追加で実装されていなければ、サイズは同じになるので上記の実装でも上手くいく。でも、後から Circle に属性が追加になったなんてことになると、見つけにくいバグになる。

Shape* と Circle* は暗黙の型変換が働くので、コンパイルエラーにならない。
下記のように書き換えると大丈夫。
(C と C は成立しないため(Cはパラメータ化されたクラス))

void general (vector<Shape>&);

でも、組み込みだとこの対策は使えない場合がある。フリーストアの問題があるので vector が使用できないため。

コンテナを使わないで上記の問題を解決方法としてインターフェイスクラスが載っている

  • インターフェースクラス
template<class T>
class Array_ref {
public:
  Array_ref (T* pp, int s) : p(pp), sz(s) { }
  
  T& operator [ ](int n) { return p[n];}
  const T& operator[ ](int n) const { return p[n];}

  bool assign(Array_ref a) {
    if (a.sz != sz) return false;
    for (int i = 0; i < sz; ++i) { p[i] = a.p[i]; }
    return true;
  }
  void reset (Array_ref a) { reset (a.p, a.sz);}
  void reset (T* pp, int s) { p = pp; sz = s;}
  inst size () const { return sz;}

private :
  T* p;
  int sz;
};

要するにフリーストアしないコンテナを自作しようということ。
これで、型の問題は解決する。

void better (Array_ref<Shape> p) {
  for (int i = 0; i < sz; ++i) p[i].draw();
}
void main() {
  Array_ref<Circle> circles;
  …circles にいくつかデータを詰める処理が入ったとして… 
  better(circles);  // エラーになる
}
    • 余談1

Array_ref が Array_ref に変換できない理由をまとめてみる。
・Shape に calcArea() という関数が定義されている
・Circle が「半径」という属性を持っている(Shape には「半径」はない)
・calcArea() をオーバーライドして「半径」を参照した処理が書かれてある
という状況で。

Shape s;
Circle c;
s = c;
s.calcArea();

s = c とした時に、サイズが合わないので c の属性「半径」が切り落とされる。その状態で s.calcArea() を実行すると「半径」を参照できないのでクラッシュ(もしくは動作不定)する。そのため、Array_ref を Array_ref に代入できない。

    • 余談2

では、Array_ref が Array_ref だとどうかというと、これもだめ。その例は以下。

Array_ref<Shape*> shapes;
Array_ref<Circle*> circles;
shapes = circles;
shapes[3] = new Rectangle(); // Rectangle は Shape の子クラス。Circle とは別階層

shapes に circles を代入したところで、shapes の実体は Array_ref。そこに Circle と親子関係にない Rectangle を代入すると訳が分からないことになる。

  • 継承とコンテナ

【インターフェースクラス】の例で、危険な代入は防げるようになった。
でも、ポリモーフィズムに対処できない。
かといって、【余談2】で書いたように Array_ref, Array_ref も代入できない。
これに対処する方法も書かれてあったが、書いてるときりがないので省略

その他

ビット演算とか、コーディング標準とかの話が書かれてあった。