演算:static_cast

Robert Schmidt
Microsoft Corporation

2000 年 5 月 18 日

長年 C++ 関連の Usenet ニュース グループを読んできた経験からすると、次の 4 つの型変換演算子の関係が、プログラマをたびたび混乱に陥れる要因になっています。

  const_cast
dynamic_cast
reinterpret_cast
static_cast

特に、多くの C++ プログラマにとって reinterpret_caststatic_cast の違いは微妙、というよりも判別不能です。今回のコラムから 2 回にわたって、これらの違いに光を当てるとともに、それぞれの形式の使い分けについての方針を示します。

概要

名前からわかるように、static_cast はある静的な型の式を別の静的な型のオブジェクトと値に変更します。C++ 規格((5.2.9/1-3 節))によれば、

static_cast<T>(v) で得られる結果は、式 v を型 T に変換して得られる結果と同じである。T が参照型の場合、結果は左辺値となる。それ以外の場合、結果は右辺値となる。型を static_cast の中で定義してはならない。static_cast 演算子によって定数性が破棄されることはない。

ある一時変数 t に関して宣言「**T t(e);」**が適正形式であれば、static_caststatic_cast<T>(e) という形式で使用することによって、式 e を型 T に明示的に変換できる。

それ以外の場合、以下に示すいずれかの変換が static_cast により実行される。static_cast を使用することでそれ以外の変換が明示的に実行されることはない。

以上より、static_cast の使用は、次のいずれかの場合に許されることになります。

  • 目的(変換後)の型のオブジェクトが元(変換前)の型の値から直接初期化できる、または

  • キャストが 5.2.9 節で後述されている(ここでは、このすぐ後に列挙した)例外のいずれかに該当する

この節には、static_cast によって「定数性が破棄されることはない」という記述もあります。標準規格の真意は、static_cast によって cv 指定をなくすことはできないということです。たとえば、static_cast によって char *char const * に変換することはできますが、char const *char * に変換することはできません。

cv 指定をなくすのに適した型変換演算子は const_cast だけです。この演算子のきちんとした説明は、const の正確さについての概論記事で行うつもりです。))

具体例

次のような宣言がある場合、

  struct B
   {
   operator int();
   };

struct D : B
   {
   };

B b;
float const f = 0;

次のような変換は、

  static_cast<void *>(&b); // equivalent to '(void *) &b;'
static_cast<int>(b);     // equivalent to '(int) b;' and 'int(b);'

どちらも適正形式です。1 番目の変換は B * から void * への暗黙の標準変換に依存しているのに対して、2 番目は b.operator int() を暗黙のうちに呼び出しています。どちらの変換も 2.5.9/1 節の一般規定に従っています。

前述のとおり、この節ではこの一般規定に対する例外もリストアップされています。それらの例外では、目的の型のオブジェクトが元の型から初期化できない場合でも、

  static_cast<int>(f); // equivalent to '(int) f;' and 'int(f);'
static_cast<D &>(b); // equivalent to '(D &) b;'

といった変換が可能です。

最後の例として、次の式のペアのうち、

  static_cast<int const *>(&f); // OK
static_cast<int       *>(&f); // error

1 番目の変換は成功しますが、2 番目は失敗します。どちらも float 型へのポインタを int 型へのポインタに変換しようとしていますが、2 番目の変換は const 指定まで削除しようとしており、標準規格から見れば禁じ手です。2 番目の変換は、より伝統的な次の形式では許されます。

  (int *) &f;

暗黙のキャスト

2.5.9/1 節によれば、次のような変換が許されるのは、

  T t = static_cast<T>(e);

構造体

  T t(e);

も適正形式の場合、つまりキャストを使わずに直接初期化できる場合です。したがって暗黙のうちに、宣言

  long l = static_cast<long>('x');

  long l('x');

が同等になります。言語仕様としてこのような冗長なキャストを認めるのはおかしいと思われるでしょう。これによって新しい機能が追加されるわけでもなく、ソース コードの中に目障りなものが増えるだけに思えます。

しかし、たとえ必要のない場合でも、このようにすべての型変換を明示的な形にすることにはメリットがあります。型変換は危険な操作です。言語規則で暗黙のうちに許される型変換でも、変換演算子やコンストラクタで明示的に行う型変換でも、危険は同じです。すべての変換操作を注意深く見ていくことで、後で問題になりそうな点をより簡単に識別することができます。

言語仕様によって結局許されてしまう変換操作を強調するため、C++ 委員会に対して 5 つ目の型変換演算子((implicit_cast))を追加する提案を出した人もいます。しかし、委員会はその提案を却下しました。その理由はおそらく、次のようなテンプレート

  template<typename FROM, typename TO>
inline TO implicit_cast(FROM const &x)
   {
   return x;
   }

によって簡単に同じものが作れてしまうからでしょう。このテンプレートがあれば、前述した例を次のように書き直せます。

  long l = implicit_cast<long>('x');

明示的な変換がまったく必要ない場所に static_cast を使うのが不安なプログラマは、自分が標準的に使用する個人用ライブラリやプロジェクト単位のライブラリに implicit_cast を定義して、static_cast の代わりに使ってみてはどうでしょうか。

明示的なキャスト

static_castimplicit_cast で十分間に合う場合にしか使えないとすれば、もはやその存在価値がありません。しかし上の例で示したとおり、static_cast を使えば、言語仕様では暗黙の変換が許されない場合でも明示的な変換が可能です。明示的な変換が許されるのは、次の場合です。

  • あらゆる型から void への変換

  • 基本型から派生型の参照への変換

  • 一部の標準変換の逆変換

void への変換

標準規格によれば、あらゆる型の式を cv 指定された void に変換できます。この規定は次のすべての式に適用されます。

  static_cast<void>(5);
static_cast<const void>("abc");
static_cast<volatile const void>('x');

いったい何のために void 型に変換するのか不思議に思われるかもしれません。void 型のオブジェクトは宣言できないことを考えるとなおさらです。その理由として頭に浮かぶのは 2 つです。

最初に、次のようなテンプレートを考えます。

  template<typename R1, typename R2>
R1 adapt(R2 (*f)())
   {
   // ...
   return static_cast<R1>(f());
   }

adapt は関数 f を受け取っています。f はパラメータを受け取らずに R2 型を返す関数です。Adapt は return 文で f() を呼び出し、f が返す型(R2)を adapt が返す型(R1)に変換しています。

(好奇心旺盛な方へ:このテンプレートの名前を adapt としたのは、標準テンプレート ライブラリの関数 - オブジェクト アダプタを真似てパターンを作成したためです)。

次のようにしてテンプレートを初期化すると、

  int g();

adapt<void>(g);

結局次の特殊化テンプレートが生成されます。

  void adapt(int (*f)())
   {
   // ...
   return static_cast<void>(f());
   }

static_castvoid に変換されるので、同じテンプレートを void 型と非 void 型の両方に使用できます。

static_cast<void> を使う理由として次に考えられるのは、式の副作用の消失です。関数

  int f();

  f();

というように関数の戻り値を使わずに呼び出すと、戻り値を使おうとしてうっかり忘れたのではないかと、後でソースを見た人に疑われる可能性があります。次のようにして明示的に戻り値を放棄すれば、

  static_cast<void>(f());

ほかのプログラマに対してプログラム作成者の意図がより明確に伝わります。

基本型から派生型の参照への変換

次のような型と、

  struct Base
   {
   };

struct Derived : Base
   {
   };

次のようなオブジェクトがある場合、

  Derived derived;
Base &base = derived;

標準規格では次のような明示的な変換を許しています。

  static_cast<Derived &>(base);

標準規格によれば、このキャストの結果は Derived 型の左辺値ですが、私はこの結果は Derived & になる(べきである)と考えています。

このキャストを使うと、たとえそれが適正形式でも、問題が発生することがあります。これについては次の例を見てください。

  Base base1;

static_cast<Derived &>(base1);

この場合、base1 は実際には Derived オブジェクトではなく、Base オブジェクトになります。この変換はコンパイラをあざむき、未定義のプログラム動作を引き起こします。標準規格には、この変換に関するコンパイル時の制限がこのほかにも挙げられています。

  • Derived * から Base * への暗黙の標準変換が存在すること

  • BaseDerived の非仮想の基本型であること

  • Derived は少なくとも Base と同様に cv 指定されていること(つまり、cv 指定を追加することはできても削除はできない)

標準変換の逆変換

標準規格の第 4 節では、次のような暗黙の標準変換が数多く挙げられています。

  • 左辺値から右辺値への変換

  • 配列および関数からポインタへの変換

  • const および / または volatile の追加

  • 整数型と浮動小数点型の間での変換および上位変換

  • ポインタ型とメンバへのポインタ型の間での変換

  • bool への変換

上記の変換の大部分は C の仕様を基にしているので、プログラマにはおなじみのはずです。これらは暗黙の変換なので、キャストなしで行われます。例として次のようなものがあります。

  char a[10];
char *p = a;       // converting array to pointer
char const *s = p; // adding const qualification
void *v = p;       // conversion among pointers
float f = 123;     // converting among integral and floating-point
unsigned u = 123;  // promoting among integrals

static_cast を使うと、上記の変換操作の方向を強制的に逆にすることで、変換を逆転させることができます。特に static_cast は、整数および浮動小数点型、ポインタ型、メンバへのポインタ型の間での標準変換と上位変換を逆転させることができます。次の例では、

  char *p;
void *v;
v = p;                      // OK, implicit conversion
p = v;                      // error, no implicit conversion
p = static_cast<char *>(v); // OK, explicit conversion

明示的な static_cast が標準変換の規定に優先しているため、許可されないはずの変換が強制的に行われています。

static_cast によって、次のような逆方向変換も可能です。

  • 整数型から列挙型への変換

  • (おそらく cv 指定された)Base * から(同じく cv 指定された)Derived * への変換

  • (おそらく cv 指定された)T Base:: * から(同じく cv 指定された)T Derived::* への変換

  • (おそらく cv 指定された)void * からあらゆる T * への変換

次のような場合、これらの変換は未定義の動作を引き起こします。

  • 整数値が列挙型の範囲を超えた場合

  • Base *Derived オブジェクトをポイントしていない場合

  • T Base:: *Derived のメンバをポイントしていない場合

  • void *T オブジェクトをポイントしていない場合

static_cast によってすべての暗黙の変換を逆にできるわけではありません。特に、次のような変換は、static_cast ではできません。

  • 右辺値から左辺値への変換

  • ポインタから配列への変換

  • ポインタから関数への変換

  • bool から bool への変換

  • cv 指定を削減する変換

以上の逆変換のうち、一部についてはほかのキャスト形式を使って強制することもできます。これ以外の逆方向への変換(ポインタを配列に強制的に戻すなど)については、この種のキャストでは不可能です。

ダウンキャストの代替方法

static_cast によって可能な変換の多くは、基本クラスから派生クラスへの変換が関わっています。このような変換には継承階層の下方向へのキャストが必要になることから、これらを「ダウンキャスト」と呼んでいます。それと対照を成すのが「アップキャスト」(派生クラスから基本クラスへの変換)で、通常の言語仕様では暗黙の変換としてサポートされています。

ダウンキャストは安全とは言えません。ダウンキャストでは、基本クラスの静的な型を持つエンティティが実際には派生クラスの動的な型であると仮定されるからです。static_cast を使ってダウンキャストすると、コンパイラはプログラマがうそをついていてもそれを信じてしまいます。本当にうそをつけば、その結果は未定義の(そしてたいていはひどい)プログラム動作となって現れます。

このコラムでは dynamic_cast は取り上げませんが、static_cast の代わりにこれを使ってダウンキャストができる、とだけ簡単に言っておきます。dynamic_cast の一番の特長は、プログラムの実行時チェック機能によってプログラマの仮定が検証される点です。(疑わしい)派生エンティティが実は派生型でなかった場合は、dynamic_cast によって(ポインタ変換については)NULL が与えられ生成され、(参照変換については)例外がスローされます。

dynamic_cast とそれに関連する RTTI(実行時型識別機能)については、将来のコラムで必ず取り上げる機会があると思います。

次回は

私の次回のコラムでは、reinterpret_cast について今回同様に概要を示し、同時に static_cast との使い分けについての方針を示す予定です。

**Robert Schmidt **** は MSDN のテクニカル ライターです。このほかに彼が寄稿している雑誌に、C/C++ Users Journal があります。そこでは、彼は編集補助およびコラムニストとして活躍しています。これまでのキャリアで、ラジオ DJ、野生生物飼育係、天文学者、ビリヤード場管理者、探偵、新聞配達夫、そして大学講師を経験しています。

Deep C++用語集