Arantium Maestum

プログラミング、囲碁、読書の話題

Effective C++勉強メモ: Item 27 キャストはできるだけ避けよう

C++は基本的に型安全。コンパイルするなら本来unsafeなことはしない」とはじまるこの項目。本当か?

「キャストしなければ・・・」と続くわけだが、それにしたって今まで読んできた中にもどれだけundefined behaviourをうっかり踏みそうな地雷を見てきたと・・・

まあ多分「規格に従って、UBはちゃんと避けて、そしてキャストは使わなければ、C++は型安全」という主張なのだろうけど、そりゃUB踏んでないのに型安全じゃなかったらそれは一昔前でいうところの「孔明の罠」だろ・・・

ルサンチマンを吐き出したところですすめよう。

C++にとって、型安全を放棄する機能であるキャストは、JavaC#のそれに比べて危険性が格段に高く、さらに使う必要性は格段に低い。あまり使わないようにするべき、というのがScott Meyersの主張。

6種類のキャスト

C++はCから受け継いだ二つのキャスト方法と新たに定義された4つのキャスト方法の合計6つがある。

  • (T) expression: Cスタイル
  • T(expression): Cスタイル
  • const_cast<T>(expression): expressionのconst性を打ち消すキャスト
  • dynamic_cast<T>(expression): 実行時にある親クラスポインタが子クラスポインタに変換できるか確認してから行う(コストの高い)キャスト
  • reinterpret_cast<T>(expression): 処理系に依存する結果をもたらすキャスト。たとえばポインタからIntに変換する。
  • static_cast<T>(expression: implicit conversionを強制的に行ったり、逆に戻したりするキャスト

Cスタイルキャストはどちらも意味は同じで、dynamic_cast以外のC++キャストができることはすべてできる。が、明示的ではないのと強力すぎるのでできれば封印してC++キャストを使っていくべき。

static_castコンパイル時の変換だと思われがちだが、例えばAを継承したBへのポインタをAへのポインタにstatic_castすると、ベースクラスのメモリ位置がどこにあるかは処理系と状況(多重継承など)に依存し、実行時にポインタの値が書き換えられる。油断しているとわからないところで実行時コストが発生する一例である。

オブジェクトをキャストすると戻り値はコピー

例えばAクラスを継承するBクラスの中でAクラスのメンバ関数を呼びたいとする。「それじゃキャストしないと」と思って下のようなコードを書いたとする:

class B : public A {
public:
  virtual void init() {
  static_cast<A>(*this).init();
  // B-specific code
  }
};

このコード、もしinitがオブジェクトの状態を変更するものだとしたら期待している挙動にはならない。なぜならstatic_castはBのインスタンスをコピーして新しいA型のオブジェクトを返し、その新しいオブジェクトに対しinitを実行し、そのオブジェクトはその後すぐ破棄される、からだ。

正しくは

class B : public A {
public:
  virtual void init() {
  A::init();
  // B-specific code
  }
};

これで現在のインスタンスに対してAのメンバ関数initが呼ばれる。

「キャストしなきゃ」と最初思ったとしても別のC++的に正しいやり方が存在するケースが多いという点の一例。

dynamic_cast

dynamic_cast処理系依存ではあるが、大抵実行時に継承ツリーを辿って文字列比較でチェックしていく。なので遅い。パフォーマンスが重要なコードではかなり気をつけて使うべきだ。

dynamic_castが使われる状況というのは親クラスへのポインタが、実際には特定の子クラスのものかどうかわからないが、もしそうだとしたらその子クラスのインターフェースを使いたい、というケースが多い。特に親クラスへのポインタのコンテナから各要素に対してそのチェックを行う、など。

  • インターフェースが違う子クラスは別のコンテナにいれる
  • 親クラスに(適切なデフォルトをもった)virtual関数として定義してインターフェースを統一する

のどちらかが推奨。それがなんらかの理由でできないときだけ、しぶしぶdynamic_castを使うべき。

さらにいうとif ... else if ... else if ...とdynamic_castしていくようなスタイルを書いてしまいがちだが、これは継承ツリーの変更に非常に弱い書き方なので絶対避けるべき、とのこと。