注意:邪悪で汚らわしい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形式のキャストは、
- const_cast
- static_cast
- static_castとconst_cast
- reinterpret_cast
- 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形式のキャストは、使ってはならない。
C++形式のキャストを多用するようになってから随分コンパイルエラーのお世話になりました。
ReplyDelete一番危険なキャストがstatic_castだという事も最近知りました。
でもSTLを多用しているとunsignedとintがどうしてもミックスしてしまうのでこれは欠かせない。
多態はあまり使いませんよ。std::str1::shared_ptrとstd::vectorの組み合わせで初めて現実的になるし。
大部分のプログラマは単に型Aを型Bに変えたいのであって、どう変えるかなんてあまり興味がないんです。コンパイラさんが良きに計らってくれればそれでいいのです。C形式がダメならauto_castとかがあればいいです。
ReplyDelete> 一番危険なキャストがstatic_castだという事も最近知りました。
ReplyDelete3つの中では一番安全なはずですよ。