2010-07-06

邪悪なC形式のキャストにしかできないこと

注意:邪悪で汚らわしいC形式のキャストは、いやしくもC++プログラマたる者は、使うべからず

C++では、玉虫色のC形式のキャストの機能を、三つに分割した。static_cast、reinterpret_cast、const_castである。しかし、この三種のキャストでは、C形式のキャストを完全に代替できないという声をよく聞く。曰く、「どうしても書けないキャストがある」と。

それはよく聞く話だが、では実際にどのようなキャストなのかということは、誰も審らかにしない。誰も知らないキャストであれば、特に使えなくても問題ないはずだ。ただし、「C形式のキャストならばできるキャストが、新しいキャストを組み合わせてもできない。どんなキャストかは知らないが、とにかくできないと聞いている。故に新しいキャストはクソだ」などという論調で、C++の改良されたキャストを使わぬC畑の外道がしゃしゃり出てくるのも困る。そこで、ここでは、C形式のキャストでしかできないキャストを、余す所無く紹介しようと思う。

その前に、static_castとreinterpret_castの違いを説明しなければならない。

reinterpret_castは、愚直なキャストである。reinterpret_castは、値を保ったまま、型情報だけ変えることのできるキャストである。だから、int *からfloat *とか、some_class *からother_class *などといった、お互いに派生関係になく、ユーザー定義の型変換関数もない型のポインターやリファレンスにも変換できる。これは、変換するというより、型だけ変えるというべきである。値はそのまま保持される。

一方、static_castは、必要ならば値を変える。これは重要である。

reinterpret_castにできず、static_castにできることは、D&Eの331ページにも書かれているように、クラス階層のナビゲーションである。派生クラスへのポインターと基本クラスへのポインター同士の相互変換の際、そのポインターの内部的な値が、変わる可能性がある。これはつまり、ストレージ上のクラスのオブジェクトの中で、そのクラスのサブオブジェクトの場所が違うということである。従って、値を変更しなければ、正しい場所を参照しない。リファレンスも内部的にはポインターなので、問題は同じである。

static_castは、ポインターの値を変更することによって、クラス階層のナビゲーションを行える。reinterpret_castは、値を変更しないので、それができない。

さて、C形式のキャストは、

  1. const_cast
  2. static_cast
  3. static_castとconst_cast
  4. reinterpret_cast
  5. reinterpret_castとconst_cast

このような順番と組み合わせで表現できる。これらのキャストを上から順番に試していき、キャストが行えるところで、そのキャストを行う。

ただし、C形式のキャストでは、static_castが、以下の三つの追加的なキャストを行うことができる。

1. 派生クラスへのポインターやリファレンスから、基本クラスへのポインターやリファレンスに変換できる。文字通り変換できる。アクセス指定などは考慮されない。

struct Base { } ;
struct Derived : private Base { } ;

int main()
{
    Derived d ;

    Base & ref1 = (Base &) d ; // OK
    Base & ref2 = static_cast<Base &>(d) ; // ill-formed
}

このキャストは、reinterpret_castでもできる。ただし、reinterpret_castは、クラス階層のナビゲーションを行わないので、正しく動かない。C形式のキャストは、クラス階層のナビゲーションを行うので、正しく動く。

とはいえ、private継承している基本クラスへのポインターやリファレンスに変換する時点で、設計が多いに間違っている。それなら最初からpublic継承すべきなのだ。

2. 派生クラスのメンバーへのポインターから、曖昧ではない非virtualな基本クラスのメンバーへのポインターに変換できる。文字通り変換できる。アクセス指定などは考慮されない。

struct Base { } ;
struct Derived : private Base { int x ; } ;

int main()
{
    int Base::* ptr1 = (int Base::*) &Derived::x ; // OK
    int Base::* ptr2 = static_cast(&Derived::x) ; // ill-formed
}

これも、アクセス指定を無視できる。reinterpret_castでは、クラス階層のナビゲーションが正しく行われない。ただし、前述の理由で、このキャストを使用したいというのは、設計が間違っている証拠である。最初からpublic派生すべきなのだ。

曖昧ではなく非virtualな基本クラスのポインターやリファレンスあるいはメンバーへのポインターは、派生クラスのポインターやリファレンスあるいはメンバーへのポインターに変換できる。文字通り変換できる。アクセス指定などは考慮されない。

struct Base { int x ; } ;
struct Derived : private Base { } ;

int main()
{
    Derived d ;

    d.x = 0 ; // ill-formed. アクセス指定のため

    int Derived::* ptr = (int Derived::*) &Base::x ; // well-formed.
    d.*ptr = 0 ; // well-formed. C形式のキャストを使ったため、アクセス指定を無視できている
}

これも、アクセス指定がらみだ。これは、実装依存だが、reinterpret_castで代替できるかもしれない。というのも、reinterpret_castの挙動のほとんどが、実装依存だから、保証はできないのだ。

これはの三種のキャストは要するに、クラスのアクセス指定を無視でき、しかもクラス階層のナビゲーションをするキャストである。このようなキャストが必要となるコードは、現実に存在しないはずだ。const_castは、忌まわしく薄汚いCとの相互利用のために、仕方のない部分がある。dynamic_castやtypeidも、必要悪な機能である。しかし、アクセス指定の無視というのは、話にならない。まず、アクセス指定はC++から追加された機能であるので、互換性の問題はない。privateやprotectedなメンバーにアクセスしたいとすれば、最初からpublicにしていればいいのだ。土台、設計が間違っている。

もしこれを読んでもまだ、「C形式のキャストはぁ~、新しいキャストではぁ~、できないキャストができるからぁ~」などと世迷言を垂れ流す者は、全プログラマの為に害悪である。二度とコードを書かないでもらいたいのはもちろんのこと、およそソフトウェア業界には一切関わらないでお貰い申したい。

注意:邪悪で汚らわしいC形式のキャストは、使ってはならない。

3 comments:

萌ゑ said...

C++形式のキャストを多用するようになってから随分コンパイルエラーのお世話になりました。

一番危険なキャストがstatic_castだという事も最近知りました。

でもSTLを多用しているとunsignedとintがどうしてもミックスしてしまうのでこれは欠かせない。

多態はあまり使いませんよ。std::str1::shared_ptrとstd::vectorの組み合わせで初めて現実的になるし。

Anonymous said...

大部分のプログラマは単に型Aを型Bに変えたいのであって、どう変えるかなんてあまり興味がないんです。コンパイラさんが良きに計らってくれればそれでいいのです。C形式がダメならauto_castとかがあればいいです。

Anonymous said...

> 一番危険なキャストがstatic_castだという事も最近知りました。
3つの中では一番安全なはずですよ。