C++ 再入門 その21 - 型キャストのあれこれ
継承したクラスの参照(ポインタ)に対して、子クラスから親クラス(アップキャスト)に変換したり、親クラスから子クラス(ダウンキャスト)に変換できる(こともある)ということを説明しました。このように型変換できるというのはクラス階層を抽象的に扱うのにとても便利なものですが、ここで型変換(型キャスト)についてあらためて整理してみましょう。
C++はC言語の文法の上に成り立っているので、まずC言語の型キャストについて復習しましょう。C言語では特に何も書かなくても「標準変換(暗黙の変換)」とも呼ばれる型変換が自動的に行われます。
Cの型変換と式
INT02-C. 整数変換のルールを理解する
これらの変換は代入の際だけではなく式の途中の演算でも起こります。よく問題となるのが取りうる値の大きな型から小さな型への変換で、目的の型に収まらないような値を持っていれば、その値が不定となってしまうことです。忘れやすいのが符号なし型が混在する場合で、式の中に符号なしの型があれば、そちらが優先されてしまうという点です。
標準変換
型変換とタイプ セーフ
これは変数だけではなく定数に対しても同じ話で、定数の型を示す文字(Lなど)を使って正しく指定しないと不用意な型変換が発生します。最近のコンパイラはこのような変換が起こる部分で何らかの警告を出すものも多いのですが、文法的には正しい以上、警告に頼ること無くこのような変換が発生していないか注意を払う必要があります。
C言語教室 解答編 第12回 と オーバーフローのはなし
もちろん、C言語でも明示的に型変換を書くことも出来ます。式の中に()で囲んだ型名を書くことで、式の値を指定した型に変換してくれます。オーバーフローする可能性があるときには、最初に評価される値に対して大きな値も収容できる型に明示的に変換しておくことで、その可能性を減らすことが出来ます。
キャスト演算子
キャスト演算子: ()
そしてC++におけるキャストに進みます。C++ではもちろん()で囲ったC言語の書き方のキャストも可能ですし、関数のような形式でキャストを書くこともできます。
とはいえC++における型変換の正式な書き方は
です。castには以下の種類があります。
static_cast 基本的にC言語のキャストと同じです。
dynamic_cast ダウンキャストを安全に行うキャストです。
const_cast const修飾子を外します。
reinterpret_cast 型情報を無視してそのまま型を変えます。
dynamic_cast と const_cast は、対象となる型が配慮を必要としない型であれば static_cast と同じ働きになります。
キャスト
C++ のキャスト
9 章 キャスト演算
もう少し丁寧に見ていきましょう。まず static_cast が基本です。C言語での使い方とほぼ同じですし、void *を特定の型に対するポインタ型への変換するときなどで使います。
const_cast は、型に const や volatile が付いている時に、これらを除去するのに使います。constを外すときは単にconstの無い型に()を使ってキャストしがちですが、この書き方をすることで「constを外しているんだよ」ということが明確になります。
そしてstatic_castではエラーとなるような互換性の無いポインタ型への変換や整数型とポインタ型間のキャストには reinterpret_cast を使います。これが最も強力なのですが、元の型に関する情報を忘れて強引に「再解釈」するものです。本来であれば union にするべき場合などでも便利に使えますが、最も注意が必要なキャストです。
ということで、最大のヤマ場が dynamic_cast です。このキャストを使えば、継承されたクラスへの参照(ポインタ)を実行時にその型情報を使って関係をチェックすることが出来、キャストが正しくない場合には型変換が無効であるとして 0 を返します。このため、このキャストを使った場合には必ず値が 0 になっていないかの確認が必要となります。もちろんアップキャストの場合などにこのキャストを使っても static_cast と同じ動作となり実行時に型情報を確認するようなコードは生成されません。
第42章 ダウンキャスト
もちろん、このキャストを使うには型情報の生成を行うようなコンパイルオプションを指定しなければなりません。これはコンパイラ時にわかるので適切なオプションを設定すれば良いだけです。
dynamic_cast 演算子
dynamic_cast の実装を読み解く
動的キャスト
C++のモヤモヤをデバッグで解消 – キャスト編
このように仮想関数の呼び出しは実行時にチェックを行うコストが掛かるのは確かなのですが、事前にコードによって適切にチェックするのが難しいので、いざ実行する時に不適切な呼び出しをしてしまったり、不適切なメモリをアクセスしてプログラムが停止してしまうよりもよっぽどマシです。ループの内側などで効率が求められる場合には、ループの外側でキャストを済ますことが出来るケースでは、一時変数を使うことも出来るかもしれません。またキャストの結果、値が 0 になることでチェックでハジカれたことがわかるので、必ず値を確認しなければ実行時にプログラムが停止することには違いないので注意しましょう。
継承されたクラスにおいて、同じメソッドを呼ぶ事でクラスごとの異なる関数を呼び出す仕組みは、とても便利ではあるのですが、その関係を正しく管理しないとならないのが少しばかり面倒ではあります。この仕組みは同じようなオブジェクトを扱うプログラミング言語では、ほぼ同じような実装になっていますし、同じような配慮が必要になります。
というところで、より複雑な継承についてに進みましょう。
ヘッダ画像は、以下のものを使わせていただきました。https://commons.wikimedia.org/wiki/File:ISO_C%2B%2B_Logo.svg
Jeremy Kratz - https://github.com/isocpp/logos , パブリック・ドメイン,
https://commons.wikimedia.org/w/index.php?curid=62851110による
#プログラミング #プログラミング言語 #プログラミング講座 #CPP #実行時型情報 #キャスト #型変換 #動的キャスト