C++ で型のキャストを使う - C++ プログラミング
PROGRAM
C++ で型のキャストを使う
C++ では「キャスト」によって、ある値のデータ型を別のデータ型として扱うことができるようになっています。
従来の C 言語にあった丸括弧による型キャストも使えますが、それを用途毎に 4 つのキャスト分離したのが C++ のキャストになります。
- static_cast<T>
- reinterpret_cast<T>
- const_cast<T>
- dynamic_cast<T>
キャストを意味で分けることで、C++ ではコンパイラが使い方の間違いを検出できるようになっています。
非ポインタに対する static キャスト
ポインタ型ではない普通の値については、次のように static_cast<T> を使ってデータ型の変換を行います。ポインタ型の値でも唯一 void* 型だけは、この static_cast を使って他のポインター型にも変換できます。
double dValue;
int iValue;
dValue = static_cast<double>iValue;
このように、変数や値の前に static_cast に続けてデータ型を括弧内で指定することで、その変数や値を指定したデータ型として扱うことができます。
このときポインタではないデータ型については、C++ 言語の仕様で規定されたルールに従って、元の値が適切な値に変換されます。
このように、必要に応じて適切な値に変換されるため、static_cast を使ったキャストが C++ ではいちばん安全なキャストになります。
もちろん値の変換が伴うため、そこで値に誤差が生じる場合があります。その影響を考慮するのはプログラマの責任になります。
また、void* ポインタをキャストする場合に限っては、キャスト後にそれを安全に使えるかどうかは、プログラマの責任です。
さて、このような static キャストの中には、プログラムしなくても暗黙的に行われるものもあります。
int a = 1.8;
double d = 3;
このように、整数型の変数に double 型の値をキャストなしで代入しても、自動的に int 型にキャストされて代入されます。
逆に double 型の変数に int 型の値をキャストなしで代入しても、オーバーフローでもしない限り、値がおかしくなることはありません。
ただし、次のような場合は明示的なキャストが必要になります。
int a = 1;
int b = 3;
double d = static_cast<double>(a) / b;
変数 a と b はどちらも整数型のため、これをキャストなしで割り算すると、計算結果も整数で取得されます。つまり、今回のように計算結果が小数点になる場合、このようなプログラムでは小数点数以下が抜け落ちてしまいます。
これを防ぐために、変数 a か b のどちらかを double 型にキャストすることで、正しい計算結果を得ることができます。
ポインタに対する再解釈キャスト
ポインタに対するキャストは、ポインターが指し示す先の「データ」がどのデータ型の値を表現しているものかを指定するために使います。
たとえば float* 型のポインタであれば、そのポインタが指す先のメモリにあるデータは float 型の値、またはそれが複数つらなる配列であることを示しています。
float* pFloat;
これをキャストするということは、そのポインタが指す先のメモリにあるデータを「キャストで明示した型として扱う」という意味合いになります。
たとえば float* 型の変数 pFloat を char* 型にキャストしてみます。
この時のキャストは reinterpret_cast<T> という再解釈キャストを使います。
char* pChar = reinterpret_cast<char*>pFloat;
こうすることで、float* 型として定義されている pFloat が指すメモリの値は、キャストして代入した char* 型の変数 pChar を通して参照すると、あたかも char 型であるかのように、というか実際に char 型の値として扱うことができるようになります。
ここで、注意したいのは static_cast のような値そのものを適切なデータに変換する処理は行われていないところです。
たとえば float であれば 4 バイトのサイズで 1 つのデータが構成されていますが、char であれば 1 バイトで 1 つのデータが構成されます。
そのため、float* 型を char* 型にキャストした場合、キャスト後のポインタから取得できる最初の値は、 4 バイトで 1 つの float を構成しているデータの最初の 1 バイトになります。
配列ポインタに見られる reinterpret_cast の特徴
この性質が特に顕著に現れるのが、配列のポインタを扱う場合です。
たとえば float* 型の配列の場合、ポインタが指すアドレスを先頭に、任意の数の float 型の値が連続して書き込まれています。
float 型は通常は 4 バイトで 1 つの値を構成しているため、2 つ目の値は先頭から 4 バイト先に、3 つ目の値はそこからさらに 4 バイト先に、といったように 4 バイト単位で記録されています。
ポインタは、足し算や引き算を使って、1 を足すと次の値が格納されているアドレスへ、1 を引くと前の値が格納されているアドレスへと移動できるようになっています。
このとき、float であれば 4 バイト単位なので、ポインタに 1 を足し算すると、値の保存先を示すアドレスには実際は 4 が足されています。
これを reinterpret_cast<char*> で char* 型に強制変換したとすると、char 型は 1 バイト単位で構成された値なので、キャスト後のポインタを足したり引いたりしたときには、ポインタが指すアドレスも、char のサイズに従って 1 足したり 1 引いたりするようになります。
この reinterpret_cast<T> によって何が嬉しいかは、その時々のプログラムによって変わりますが、たとえば任意のデータ型の配列を 1 バイト単位で処理したいようなときに char* 型にキャストすることで、それが実現できたりもします。
この reinterpret_cast<T> は、そういった高度な操作をするために使うキャストなので、これを使わず static_cast<T> だけで済ませられるならそうした方が、安全なプログラムをくめると思います。
void* 型のキャストには、最適と思えるキャストを選ぶ
void* 型についてもこの reinterpret_cast を使ってキャストできますが、static_cast を使ってキャストすることもできます。
どちらでも動作は同じなので、どちらの方を書いたらいいかは意味合いで選ぶといいと思います。
static_cast は「安全」な意味合いがあるので、あるデータ型のポインタを汎用的に void* ポインタで受けた後、それをまた元のデータ型に戻すようなときには static_cast が意味的に似合うと思います。
reinterpret_cast には「再解釈」という意味合いがあるので、たとえば long 型の配列として定義したポインタを 1 バイト単位で処理するために char* 型にキャストするような、本来とは違うデータ型としてキャストしたい場合に似合います。
読取専用を解除する const キャスト
C++ では const キーワードが指定されている変数は、その値を書き換えることが禁止されます。
それでも書き換えが必要になることがあった場合、const_cast<T> を使用します。
const int roValue = 10;
int& rwValue = const_cast<int&>(roValue);
この場合、変数 roValue には const が指定されているため、これに代入しようとするとコンパイルエラーになるのですけど、この roValue 変数を const_cast<T> キャストを使って int& 型の変数 rwValue に参照を取得することで、rwValue を通して roValue の値を書き換えることができるようになります。
このとき const_cast では、int 型の値は int& 型の参照にしか変換できないところに注意します。
これを他のたとえば double& 型の参照にしたりしようとするとコンパイルエラーが発生します。また、同じ int 型でも const_cast<int> や const_cast<int*> はエラーになります。
混同しがちなのは const int 型を int 型に変換するときでしょうか。
このとき const_cast<int> はエラーになりますが、static_cast<int> はエラーになりません。
ただしこれは、キャスト前の const int を読み書き可能にしているのではなく、キャスト前の const int の値を、新しい、読み書き可能な int 型の変数に代入しているだけです.
そのため、キャスト先の値を書き換えたところで、キャスト前の値は書き換わることはありません。
クラスの継承関係を加味した dynamic キャスト
C++ には、クラスの継承関係も加味してキャストを行える dynamic_cast<T> が用意されました.
これは、他の static_cast<T>, reinterpret_cast<T>, const_cast<T> とは違って、キャストが成功したかどうかの結果は、最終的には実行時に判定することで判るようになっています。
この dynamic_cast<T> は、クラスを適切にキャストするためのものなので、クラス型以外の値またはポインタをキャストしようとした場合には、コンパイルの時点でエラーになります。
クラス型であっても、ポインター型と参照型だけがキャストできて、普通の値としてのクラス型を指定するとコンパイルエラーです。
無事にコンパイルが通っても、確実にキャストができたかどうかが判るのは、そのキャストを実行した時点になります。
代入先がクラスポインターだった場合、キャストに成功した場合は代入先の変数にポインタが格納されます。失敗した場合は nullptr (NULL) が格納されるので、それを判定することで、状況に応じたプログラムを実行することができます。
CMyClassA class1;
CMyClassB* class2 = dynamic_cast<CMyClassB*>(&class1);
if (class2 != nullptr)
{
// キャストに成功した場合の処理です。
}
else
{
// キャストに失敗した場合の処理です。
}
代入先がクラス参照だった場合は、キャストに失敗した場合は std::bad_cast 例外エラーが発生します。
try
{
CMyClassA class1;
CMyClassB& class2 = dynamic_cast<CMyClassB&>(class1);
// キャストに成功した場合の処理です。
}
catch (std::bad_cast exception)
{
// キャストに失敗した場合の処理です。
}
このようにして、状況に合わせたプログラムを記述して行きます。
dynamic_cast<T> によるクラス継承の判定
dynamic_cast<T> がなぜこのような仕様になっているかというと、クラスには「継承」という考え方があるためです。
オブジェクト指向の性質上、たとえば CPersonalComputer というクラスがあったとき、そこからさらに CWindows や CLinux、CMac といったクラスが CPersonalComputer を継承して作られたりします。
さらには CWindows から、CWindowsVista や CWindows8 クラスが作られるかもしれません。
そういったクラス全てを C++ では CPersonalComputer* 型のポインタに格納して、共通のコードで一緒くたに扱えるようになっています。
CPersonalComputer* computers[5];
computers[0] = new CWindowsVista();
computers[1] = new CMacMini();
computers[2] = new CCentOS();
computers[3] = new CMacBookAir();
computers[4] = new CWindows8();
for (int index = 0; index < 5; index++)
{
computers[index]->powerOn();
}
このように操作できる訳ですが、このとき、インスタンスがあるクラスを継承していた場合には、それ固有の処理をしたい場合があります。
それを判断するのが dynamic_cast<T> です。
dynamic_cast<T> を使うことで、あるインスタンスがキャスト先のクラスと同じか、またはそれを継承したクラスである場合にだけ、そのポインタまたは参照を取得できます。
たとえば、computers[index] が CMac を継承していた場合に特別な処理をしたい場合は、CMac クラスにキャストして命令を実行することになります。
ただ、CWindows8 クラスのように CMac を継承していないクラスの場合もあります。
そういったときに、dynamic_cast<T> は nullptr を返すため、取得できたポインタが nullptr ではないときに限って固有の処理を実装することで、CMac を継承していた場合専用のコードを安全に記載できるようになります。
for (int index = 0; index < 5; index++)
{
computers[index]->powerOn();
CMac* mac = dynamic_cast<CMac*>(computers[index]);
if (mac != nullptr)
{
// ここで CMac を継承していた場合の、固有のプログラムを記述できます。
}
}
このように、キャストと継承判定を合わせて行えるので、クラスに固有のプログラムを効率よく実装することが可能になります。
[ もどる ]