ニコニコC++入門

入門サイトや市販の入門書では絶対に教えてくれない、C++の本当の使い方を教えます。

キャストとプログラムの品質

2005-07-24 22:29:23 | C++

foo(int *arg)
{
  int work = arg;
}

 もしコンパイラが上記のプログラムに対してエラーを出さなかったとしたらどうでしょうか?

 C言語とC++言語は、Pascal ほどではありませんが、型のチェックが比較的厳しいプログラミング言語です。上記の例のような、うっかり間違って想定外の変数に代入しようとしてしまうような初歩的なバグを、コンパイルの時点でチェックすることができればバグの数も自然と減ります。

 コンパイルエラーを解決していくだけで多くのバグが取れるというのは実は画期的なことです。エラーのチェックが甘い、あるいは勝手に自動変換してしまう言語もありますが、それらの言語ではやはりデバッグに苦労します。ある程度とはいえ、コンパイラがバグを取ってくれるのですから、これを有効に利用しない手はありません。

foo(int *arg)
{
  int work = (int)arg;
}

 この厳しい型チェックはよく裏目に出て不便を強いられますので、C言語ではキャストを使用して強制的に型を変換できるようになっています(上)。しかしながら、C言語のキャストはすべての型チェックを無効にできる、たいへん強力かつ危険なもので、せっかくのエラーチェックを台無しにしてしまいます。そこでC++ではキャストが拡張されました。

foo(int arg)
{
  short work = static_cast<short>(arg);
}

  このC++で拡張されたキャストは、一見無意味で面倒なだけのようなものに見えるかもしれませんが、本当に無駄ならこのような拡張はなされないわけです。拡張されたキャストはバグを未然に防ぐ強力なツールとなります。上記の static_cast は、キャストをつけなくてもワーニングで済む種類のキャストに対してのみ使用できるキャストです。const 属性の変数に対する代入や、ポインタへの変換などは、代入が許されずにエラーとなります。

 C++には static_cast の他にも、const_cast、dynamic_cast、reinterpret_cast という4種類のキャストが用意されています。static_cast についてはすでにお話ししましたので、残りの4つを解説していきましょう。

 const_cast は const を無効化するためのキャストです(逆もできますけど)。C言語でのプログラミングにある程度精通されている人でも、const を使ったことが無いという人は多いかもしれません。const とは静的で変更不可能な定数のことです。この値はプログラムコード領域やROM領域などに置かれます。const を宣言した変数に対して変更を加えるとコンパイルエラーとなります。メモリが豊富なパソコンやワークステーションではエラーチェックされてもあまりうまみがありませんし、define や enum で済ませたほうが都合のよいことも多いでしょう。

 C++では const の重要度が飛躍的に増しています。C言語の const とC++の const は別のものだと言う人もいます。そこで、const_cast の話をする前に const の話をしておきます。

class CFoo
{
private:
  int m_nCount;
  int m_nMax;
protected:
  virtual bool CheckCount() const
    { return true; }
public:
  int SetCount(int count)
    { return m_nCount = count; }
  bool CountDown();
};

bool CFoo::CountDown()
{
  if (!CheckCount())
    return true;
  m_nCount++;
  return m_nCount >= m_nMax;
}

 上記のクラス CFoo は SetCount で指定した値まで、CountDown を使って1つづつカウントするクラスです。

 CBar は CFoo を継承しています。CFoo と同じ動作をしますが、CheckCount を変更しています。CFoo では CheckCount が false ならカウントを強制的に終了させることができるようになっており、CBar ではこれを利用して時間制限を設けています。

class CBar : public CFoo
{
protected:
  virtual bool  CheckCount() const;
private:
  bool IsTimeOver() const;
};

bool CBar::CheckCount() const
{
  SetCount(time); //error
  CountDown(); //error
  return IsTimeOver(); // no error
}

 ところが CBar::CheckCount はコンパイルできません。const が指定されたメソッドではメンバ変数の変更が禁止されるためです。CheckCount はメンバ変数の操作が禁止されており、またメンバ変数を操作するメソッドを呼び出すことも禁止されています。メンバ変数を変更するあらゆる手段が遮蔽されているのです。アクセスできるのは、メンバ変数を変更しないことが約束されているメソッド、すなわち const なメソッドだけです。

 const を利用すると、ヘッダファイルを見るだけでメソッドがメンバ変数を変更するかどうかが一目でわかるようになるため、エラーチェック以上にバグを減らす効果があります。

Sub(char *s);

Foo1(const char *p)
{
  Sub((char *)p); // no error
}

Foo2(const int p)
{
  Sub((char *)p); // no error
}

Bar1(const char *p)
{
  Sub(const_cast<char *>(p)); // no error
}

Bar2(const int p)
{
  Sub(const_cast<char *>(p)); // error

 エラーチェックに有効な const ですが、キャストするだけで const 属性を外すことができてしまいます。関数 Foo1 は引数 *p の const 属性を取りたいだけなのですが、キャストを使用すると Foo2 のようなバグでもエラーがチェックされなくなってしまいますので、お勧めできません。

 const_cast を使用すれば、根本的な型変換をさせずに、const や volatile 属性を付け外しできるようになります。Bar2 はコンパイルエラーとなりますので比較的安全です。またあとからソースを見直したとき、このキャストが const を外すことを目的としたキャストであると一目で判断できます。
 
 先ほどの CheckCount のような、メソッドに対して課した制限を無効にすることもできてしまいますので、メソッドに const を指定しても結局あまり意味がないように思われるでしょうか。

 const メソッドを設定するのは、クラスを作成する人のためではなく、クラスを使う人のためだと考えてください。実のところ、クラスを作成する立場からみると const なメソッドにはあまり恩恵がないのですが、クラスを利用する人にとっては大変役立ちます。クラスを作る場合、それを利用する立場で const を設定していくといいでしょう。

 const なメソッドの中でも、参照カウンタの増減など利用者からは見えない秘密の仕組みを操作したいことが度々あります。そういう場合、とりあえず const メソッドにしておいて見かけ上では値を操作しないように装います。利用者から見れば、メソッドが内部の値を操作していないように見えるわけです。

 このような秘密の仕組みを操作するためには、メソッドの中で const 属性を変更する必要がありますが、そこで古いタイプのキャストを使用すると、あらゆるエラーチェックが無効になってしまいます。const_cast は通常のキャストと違って const 属性を変更する以外の変換が禁止されますので、比較的安全です。const 属性はクラスを利用する人のためのものですが、const_cast はクラスを作る人のためのものだと言えるでしょう。

 次は dynamic_cast です。dynamic_cast は通称ダウンキャストと呼ばれ、C++のキャストの中でも特に複雑な事情があります。

 サブクラス(継承先のクラス)として作成したオブジェクトを、スーパークラス(継承元のクラス)としてアクセスするのは簡単です。スーパークラスのメンバ変数や関数に対しては、特別なことをせずともそのままアクセスすることができます。しかし逆の場合はそう簡単にはいきません。

class CItem
{
public:
  virtual int GetTanka() const;
}; 
class CHammer : public CItem
{
public:
  virtual int GetTanka() const;
  int Attack();
}; 
class CPotion : public CItem
{
public:
  virtual int GetTanka() const;
  int Drink();
};
 
void Sell(CItem c)
{
  c.GetTanka();
  c.Attack();  // error!

 ある基礎データクラスを作成し、そのクラスを継承させて、すべてのデータに対して一定の操作をしたい場合があります。

 上記の例では、アイテムは必ず CItem を継承するようにしています。こうすることで、アイテムの売買を1つの処理で記述できます。

 そのクラスが CHammer だろうと CPotion だろうと、CItem を継承している限り、売買処理を記述するのは1ヶ所だけで済みます。

 ところがこの構造が裏目に出る場合があります。売買処理の中に、あるクラスだけに発生する特殊な例外処理を記述したくなったとしてもできません。上記の例のように、もし売ろうとしているアイテムが武器ならば、残された攻撃力に応じて価格を変動させたいようなことがあるかもしれません。

 こうした特殊処理は、できるならば仮想関数をうまくつかって対処できればそれにこしたことはないのですが、規模が大きくなり処理が複雑になってくるとそういうわけにもいかなくなってきます。あるクラスの事情のためだけに、数百に及ぶほかのクラスを書き換えるのは現実的ではありません。

 そこでC++ではインスタンスをスーパークラスからサブクラスにキャストするための構文 dynamic_cast が用意されています。

void Foo(CItem *item)
{
  CHammer  *pItem = (CHammer *)item;  //コンパイルは通るけど、本当に正しい?
}

 dynamic_cast を使ってキャストすれば、ヌルポインタかどうかを確認することで正しくキャストされたかどうか、つまりスーパークラスをサブクラスに変換できるかどうかを確認することができます(上記)。

void Foo(CItem *item)
{
  CHammer  *pItem = (CHammer *)item;  //コンパイルは通るけど、本当に正しい?
}

 dynamic_cast を使用しなくても、C言語のキャストを使用することで無理やりサブクラスにキャストすることができますが、これは安全ではありませんし、そのサブクラスへのキャストが正当なものかどうか判断するためには、まわりくどい仕組みを用意してやらなければならなくなるでしょう(上記)。

 ダウンキャストはたいへん強烈な印象を与える仕組みですので、変わった挙動に目を奪われがちですが、dynamic_cast が用意された本来の目的は、安全なコードを書くためだということを忘れないで下さい。C言語形式のキャストでもダウンキャストは可能ですが、dynamic_cast を使用することでコードの安全性が向上するのです。

 最後に残るキャストは reinterpret_cast という、なんとも複雑な名前のキャストです。これは主に全く異なる形式のポインタを変換するために使用するためのものです。

 C++のキャストは、キャストの目的を明確にします。const_cast なら const を外すことを目的に、static_cast なら基本的な型変換を目的にしていることが一目でわかり、コードの可読性があがります。

 こうしてC++で拡張されたキャストはいずれも直接的にプログラムの流れを制御するようなものではありませんから、これらを用いずともプログラムは作れるのですが、品質のよいプログラムを効率よく製作するためにこうした仕組みを積極的に利用すべきです。コメントをきちんと書いたり、assert を入れたりするのと同じように、キャストもC++方式を採用すれば、品質はきっと向上するはずです。


最新の画像もっと見る