型変換

型変換とはプログラムにおいて、あるデータ型を他のデータ型に変換することである。型キャストとも呼ばれる。

1. 分類
-----

1.1. 暗黙の型変換と明示的型変換
------------------

暗黙の型変換は、明示的に指定しなくてもコンパイラの判断によって自動的に行われる型変換で、型強制ともいう。逆に、明示的に指定して行う型変換を明示的型変換という。

暗黙の型変換では、たとえばある式の中に複数の型の変数がある場合、すべての変数を最も上位の型に変換する。

double d; long l; int i; /* ... */ if  d = i; if  l = i; if  d *= 2;

このC言語のコードでは、dとlとiは異なったデータ型をもっているので、すべての演算は自動的に同じ型に変換された後に行われる。より大きなサイズの型に変換されることを拡大変換  と呼び、より小さなサイズの型に変換されることを縮小変換  と呼ぶ。

暗黙の型変換には注意しなければならないこともある。たとえばdouble型の値をint型の変数に代入する場合、小数点以下の端数があったり、元の値がint型で表現できる範囲を超えていたりすると、縮小変換により情報の一部が失われてしまう。浮動小数点数特有の誤差により、意図せず切り捨てられてしまうこともありうる。また、符号付き整数と符号無し整数との間の暗黙変換に関しても、同様に情報が失われたり、整数オーバーフローにより意図しない値になってしまったりする可能性がある。C言語およびC++では暗黙の縮小変換を許しているが、情報が失われる可能性のある変換に対して、通例コンパイラが警告を出す。JavaやC#などの後発言語では、暗黙の縮小変換を許可せず、後述するキャスト  構文による明示的な変換が必要となる。さらに型の扱いが厳密なF#などの関数型言語では、暗黙の拡大変換も許可せず、キャスト構文による明示的な型変換が必要となる。

1.2. 組み込みの型変換とユーザー定義の型変換
------------------------

基本的な型変換は、多くのプログラミング言語処理系で最初から定義されており、通例プロセッサ  によってサポートされる高速なハードウェア変換命令にコンパイルされる。一方、ある型から別の型への変換をユーザーが定義できる言語もある。

例えばC++では、ユーザー定義型の中に変換元の型を一つだけとる引数付きコンストラクタを定義すれば、ユーザー定義の暗黙の型変換が定義できる。これを変換コンストラクタ  と呼ぶ。コンストラクタにexplicit修飾子をつけると暗黙の型変換が許されなくなり、明示的型変換が必要となる。

class Class1 { }; class Class2 { public: explicit Class2 { /* ... */ } };

void test { Class1 c1; Class2 c2 = c1; // explicit 修飾子がなければ Class2 c2 = c1; でよい。 // explicit の有無にかかわらず、Class2 c2; と書くことは常にできる。 }<WtXmlEndTag />

ここで、Class1とClass2の間には継承関係がないにもかかわらず代入ができている。これはClass1からコンストラクタを通してClass2に型変換されるからである。

なお、上の例では型変換の構文をとってはいるが、実際の処理としてはc1はコンストラクタへの引数として渡されている。そのため、本来必要のないc1のコピーが生成される。これを避けるために、変換元の型がユーザー定義型である場合には、通常は引数を参照として渡す。また、型変換という操作の意味を考えれば、変換元のインスタンスに変更を加えるということはあり得ないので、通常は引数にconst修飾子をつけて変更不可とする。結局、コンストラクタの宣言はexplicit Class2 { ... } のように書くことが多い。

なお、C++11以降では複数の引数を持つコンストラクタであっても、explicitを指定しない場合は変換コンストラクタとなることができる。

1.3. キャストとその分類
--------------

C言語とその流れにある言語では、キャスト演算子によるキャスト式により、式の右辺値を指定された型に型変換する。この構文をキャストと呼ぶ。C言語のキャスト演算子は、型名を括弧で囲んだ形式  であり、目的の式に前置する。

double d = 1234.5678; int x = d;

C++では従来のC言語形式のキャスト構文のほか、用途および意味を明確にした4つの異なるキャスト構文が用意されている。C++では意味が曖昧なC言語形式のキャスト構文は推奨されず、状況に応じて4つのキャスト構文を使い分けることが推奨される。

1.3.1. アップキャスト
--------------

あるクラスBaseと、Baseから派生したクラスDerivedがあるとする。アップキャストとは、派生クラスから基底クラスへの型変換、すなわちDerivedのインスタンスをBaseに変換する操作である。「DerivedのインスタンスはBaseのインスタンスである」ことは保証されているので、この変換は安全である。そのため、多くの言語において、アップキャストは暗黙的に行うことができる。例外として、F#は暗黙変換を許可しない言語であり、インターフェイスのメソッドはそのインターフェイスを実装したクラスのインスタンスから直接呼び出すことができず、まずインターフェイス型への明示的なアップキャストが必要となる。

1.3.2. ダウンキャスト
--------------

ダウンキャストはアップキャストの逆で、基底クラスから派生クラスへの型変換、すなわちBaseのインスタンスをDerivedに変換する操作である。Baseのインスタンスは必ずしもDerivedのインスタンスとは限らないので、この変換は一般に安全ではなく、エラーが発生する可能性がある。そのため、多くの言語ではキャスト構文による明示的な変換の記述が必要である。通例、オブジェクト指向プログラミングではポリモーフィズムを使うべきであり、ダウンキャストおよびクロスキャストが必要になるということはプログラムの設計に問題があることを示唆している。

C++では、安全なダウンキャストのためにdynamic_castという特別な構文が用意されている。この構文では、実行時型情報を参照し、ポインタ間の変換が失敗すると結果としてnullptrが返る。参照間の変換が失敗するとstd::bad_cast例外がスローされる。dynamic_castを使用するためには、型に仮想関数テーブルが必要となる。つまり、基底クラスに少なくとも1つの仮想関数を持つ必要がある。確実に成功することが分かっているダウンキャストの場合はstatic_castで代用でき、これはdynamic_castよりも実行時コストが小さくなるが、失敗する可能性のある場合には使えない。単一継承の場合はダウンキャストにC言語形式のキャスト構文を使うこともできるが、多重継承の場合はダウンキャストにdynamic_castまたはstatic_castを使う必要があり、またどの基底クラスへのポインタにキャストするかによって結果アドレスが変化しうる。

Javaでは、ダウンキャストに失敗するとname=java.lang.ClassCastException例外がスローされる。C++のdynamic_castに相当する機能は存在しないが、instanceof演算子で型情報を問い合わせることはできる。Java 14では、instanceof演算子の機能が拡張され、末尾で宣言した変数に変換結果を格納できるようになった。

C#では、ダウンキャストに失敗するとSystem.InvalidCastException例外がスローされる。参照型またはnull許容型への変換についてはas演算子が用意されており、変換が失敗した場合はnullが返る。is演算子は、左辺式の型と右辺で指定した型との互換性をbool型の値で返す。C# 7.0では、is演算子の機能が拡張され、変換可能性をbool値で返すと同時に、末尾で宣言した変数に変換結果を格納できるようになった。C# 9.0以降では、否定パターンis notもサポートされるようになった。as演算子およびis演算子ともに、通常のキャスト演算子とは異なり、ユーザー定義変換は行われない。

1.3.3. クロスキャスト
--------------

あるクラスDerivedが、二つの基底クラスBase1とBase2を多重継承しているとする。このとき、例えばBase1からBase2のように基底クラスどうしの間で型変換することをクロスキャストという。変換する対象がDerivedのインスタンスであればキャストは成功するが、それは実行時にならないと分からないので、ダウンキャストと同様に安全な型変換ではない。

C++では、ダウンキャストと同じ構文dynamic_castで安全なクロスキャストが行える。

C++/CLIおよびC++/CXではそれぞれ、safe_cast構文によるマネージ型間あるいはWindowsランタイム型間のダウンキャストおよびクロスキャストをサポートし、失敗するとSystem.InvalidCastException例外またはPlatform::InvalidCastException例外がスローされる。

1.3.4. 静的キャスト
-------------

整数どうしの型変換や整数と浮動小数点数との間の型変換などの、ごく一般的な型変換。内部的には64bitで表すデータを32bitなどに変換する縮小変換や、32bitで表すデータを64bitなどに変換する拡大変換を伴う場合もある。一例としては、JavaやC#のintからlongへの型変換はサイズ長を倍化させる拡大変換であり、逆にlongからintへの型変換はサイズ長を半減させる縮小変換である。

2. 脚注
-----

3. 関連項目
-------

型システム
汎整数拡張