【C++】キャストとは?|static_cast・dynamic_cast・reinterpret_cast・const_castの使い分け

記事のアイキャッチ。【C++】キャストとは? 4種類の使い分けを初心者向けに解説 C++

今回は、C++でよく出てくる「キャスト」とは何か、そして4種類あるキャストの使い分けについて解説していきます。

(int)x って書けば変換できるのに、なんでわざわざ長い名前を使うの?」
「static_castとreinterpret_cast、どっちを使えばいいのか分からない」
「dynamic_castは重いから使うなって聞くけど、じゃあ何のためにあるの?」

こんな疑問はありませんか?

C++のキャストは、ひとことで言うと「この値を別の型として扱ってくれ」とコンパイラに伝える指示です。C言語では (int)x という書き方ひとつで済んでいたものが、C++では危険度に応じて4種類に分かれています。分かれている理由さえ掴めば、どれを使うかは迷わなくなります。

この記事では、4種類それぞれの役割・使い分けの早見表・実際に動かした実行結果・私がハマった失敗まで解説します。読み終えると、「とりあえず括弧で囲む」書き方を卒業して、意図が伝わるコードが書けるようになりますので、ぜひ最後まで読んでください。

キャストとは?|そもそも何をしているのか

キャストは型変換のことです。ただし「変換」と言っても、中身は大きく2種類に分かれます。

  • 値を作り直す変換int3 から float3.0f を作る。ビットの並びは別物になる
  • 解釈だけ変える変換… メモリはそのままで、「このアドレスを別の型として読む」と宣言するだけ

C言語スタイルの (int)x は、この2つを含む全部の変換を、たった1つの記法でまかなってしまいます。書くのは楽ですが、読む側からは「安全な変換」なのか「メモリを無理やり読み替える危険な変換」なのか区別がつきません。しかもコンパイラも止めてくれません。

そこでC++は、やりたいことごとに名前を分けました。これが4種類のキャストです。

  • static_cast… 普通の変換。日常の9割はこれ
  • dynamic_cast… 継承関係を実行時にチェックしながら変換する
  • reinterpret_cast… メモリの解釈を強引に変える。最終手段
  • const_cast… constを外す。基本は使わない

①static_cast|まずはこれを覚えれば足りる

static_cast「筋の通った変換」専用です。数値どうしの変換や、void* を元の型に戻す場面で使います。一番よく出てくるのが、整数どうしの割り算です。

▼アスペクト比の計算
const int SCREEN_WIDTH  = 1280;
const int SCREEN_HEIGHT = 720;

// これは 1280 / 720 が先に int の割り算になり、答えが 1 になる
float ng = SCREEN_WIDTH / SCREEN_HEIGHT * 1.0f;

// 割る前に float にしておくのが正解
float ok = static_cast<float>(SCREEN_WIDTH) / static_cast<float>(SCREEN_HEIGHT);

もう一つの定番が、void* から本来の型へ戻すパターンです。DirectXなどのAPIは「型を知らないメモリ」を void* で返してくるので、受け取った側が元の型に戻してから使います。

▼頂点バッファへの書き込み
D3D11_MAPPED_SUBRESOURCE msr;
context->Map(vertexBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &msr);

// msr.pData は void*。元の型に戻してから配列として触る
VERTEX_3D* vertex = static_cast<VERTEX_3D*>(msr.pData);
vertex[0].Position = XMFLOAT3(0.0f, 0.0f, 0.0f);

context->Unmap(vertexBuffer, 0);

ここで大事なのは、void* から戻すのは reinterpret_cast ではなく static_cast だという点です。「元々その型だったものを、その型に戻しているだけ」なので、無理やりな読み替えではありません。ここを reinterpret_cast で書いてしまうと、読む人に「何か危ないことをしている」と誤解されます。

②dynamic_cast|「本当にその型か?」を実行時に確かめる

dynamic_cast継承関係の中で、親のポインタから子のポインタへ安全に降りるためのキャストです。ゲームでいうと「敵リストに入っている Enemy* のうち、ボスだけ特別な処理をしたい」という場面です。

▼敵の中からボスだけを探す
class Enemy {
public:
    virtual ~Enemy() {}   // ← これが無いと dynamic_cast はコンパイルすら通らない
};
class Slime : public Enemy {};
class Boss  : public Enemy {
public:
    void CastSpell();
};

void Update(Enemy* e) {
    // 中身が Boss なら Boss* が返り、違えば nullptr が返る
    Boss* boss = dynamic_cast<Boss*>(e);
    if (boss) {
        boss->CastSpell();
    }
}

ポイントは2つあります。まず失敗したら nullptr が返るので、if でそのまま判定できること。もう一つは、基底クラスに仮想関数(実用上は仮想デストラクタ)が1つも無いと使えないことです。実行時に型を調べるための情報が、仮想関数テーブルにぶら下がっているからです。

「重いから使うな」と言われがちなのは、この実行時チェックのぶんコストがかかるためです。毎フレーム何千回も回すループの中では避けたほうが無難ですが、初期化時や当たり判定の分岐など、回数が知れている場所なら普通に使ってよいと考えています。

③reinterpret_cast|ビット列の解釈を強引に変える

reinterpret_castメモリの中身はそのままで、「この並びを別の型として読め」とだけ指示するキャストです。値を作り直しません。

▼floatの中身を覗く
float f = 1.0f;

// 4バイトの並びを、そのまま符号なし整数として読む
uint32_t bits = *reinterpret_cast<uint32_t*>(&f);

// bits は 0x3F800000 になる(1.0f のIEEE754表現)

static_cast<uint32_t>(f) なら答えは 1 です。値として変換するからです。一方 reinterpret_cast0x3F800000同じ「float から整数へ」でも、やっていることが全く違うのが分かると思います。

使いどころは、ファイルから読んだバイト列を構造体として解釈する、ハッシュを計算するためにビットを整数として扱う、といった低レイヤーの処理に限られます。普段のコードで出てきたら、まず設計を疑ったほうがいい種類のキャストです。

④const_cast|constを外す、最後の逃げ道

const_castconstvolatile を付け外しするためだけのキャストです。型そのものは変えられません。

▼constを受け取る古いAPIに渡すとき
// 昔ながらのC APIが、書き換えないのに char* を要求してくる
void LegacyPrint(char* text);

void Show(const std::string& s) {
    // 中身を書き換えないと分かっているので、constだけ外して渡す
    LegacyPrint(const_cast<char*>(s.c_str()));
}

これが唯一まともな使い道です。「相手のAPIの都合でconstが邪魔」というときだけ使います。逆に、本当にconstとして作られた変数のconstを外して書き換えるのは、未定義動作です。動くこともありますが、動く保証はありません。次の実行結果でその怖さが見えます。

実際に動かしてみた実行結果

ここまでの4種類を小さなコンソールアプリにまとめて、Visual Studio(MSVC、C++17)でビルドして実行した結果がこちらです。

[1] static_cast : int どうしの割り算
    キャストなし : 1.000000
    static_cast  : 1.777778

[2] static_cast : void* から本来の型へ戻す
    back[0]=10 back[1]=20 back[2]=30

[3] dynamic_cast : 中身が Boss のときだけ成功する
    Boss -> Boss* へのキャスト成功
    Boss::CastSpell() を呼べた
    Slime -> Boss* へのキャストは nullptr(失敗)

[4] reinterpret_cast : float のビット列を整数として覗く
    1.0f のビット列 = 0x3F800000

[5] C スタイルキャストは const も黙って外す
    hp        = 100
    *p        = 9999
    &hp == p  = true

注目してほしいのが[5]です。const int hp = 100; のアドレスをCスタイルキャストで int* にし、9999 を書き込んでいます。結果、同じアドレスを指している(&hp == p が true)のに、hp は100のまま、*p は9999という矛盾した状態になりました。

これはバグではなく、コンパイラが「hp はconstだから100に決まっている」と判断して、読み出しをメモリアクセスごと100に置き換えてしまったからです。これが「constを外して書き換えるのは未定義動作」の実態です。エラーも警告も出ないまま、読む場所によって値が違うという、一番デバッグしたくないバグの形です。

使い分けの早見表

キャストやること代表的な場面使用頻度
static_cast筋の通った値の変換・型を戻すint↔float、void*→元の型、安全なアップキャスト日常使い(9割)
dynamic_cast実行時に型を確かめてから変換Enemy*→Boss* のダウンキャスト必要なときだけ
reinterpret_castビット列の解釈を強引に変えるバイナリ解析、ハッシュ計算本当に必要なときだけ
const_castconstを付け外しするconst非対応の古いAPIに渡す基本使わない
(型) のCスタイル上の4つを全部兼ねるC++では使わない

迷ったらまず static_cast を書いてみて、コンパイルが通らなかったら「自分は危ないことをしようとしている」と気づける、というのが一番実用的な使い方だと思います。Cスタイルだとこの「気づき」が一切得られません。

【重要】私が実装中にハマった体験談

自主制作のDirectXサンプルを見直していたときに、キャストが全部Cスタイルになっていることに改めて気づきました。頂点バッファの (VERTEX_3D*)msr.pData、アスペクト比の (float)SCREEN_WIDTH、テクスチャサイズの (int)metadata.width などです。

動いているので放置していたのですが、「桁が減る変換(ナローイング)をしている箇所を全部洗い出したい」と思ったときに、完全に手が止まりました(int) で検索しても、安全な変換も危険な変換もコメントも全部同じ顔で並ぶからです。metadata.widthsize_t(符号なしで64ビット)なので、int への代入は本来チェックしたい箇所ですが、Cスタイルだと警告すら黙って消されます

ここで学んだのは、static_castなどの長い名前は「面倒な規則」ではなく、あとで自分が検索するための目印だったということでした。「危ないキャストを全部見せて」が reinterpret_castconst_cast の検索だけで済むのは、規模が大きくなるほど効いてきます。

もう一つ、この記事のために上のサンプルを書いていて自分でも予想外だったのが、[5]のconst_castの結果です。「未定義動作なのでやめましょう」とは知っていたものの、実際に走らせるまでは「どうせ書き換わってしまうんだろう」と思っていました。実際には書き換わらないどころか、同じアドレスなのに読み方で値が分岐するという、もっと質の悪い壊れ方をしました。

キャストのよくある失敗例と対処法

①割り算の答えが必ず0や1になる

int どうしの割り算は、結果をfloatに入れても手遅れです。上の実行結果[1]のとおり 1280 / 720 の時点で 1 になってしまっています。割る前に片方を static_cast<float> するのが正解です。アスペクト比や命中率の計算で定番のミスです。

②dynamic_castがコンパイルエラーになる

「C2683: dynamic_cast はポリモーフィック型ではありません」のようなエラーが出たら、基底クラスに仮想関数が1つも無いのが原因です。対策は簡単で、基底クラスに仮想デストラクタ virtual ~Enemy() {} を追加すれば通ります。どうせ親のポインタで delete するなら仮想デストラクタは必須なので、これは一石二鳥です。

③キャストしたら落ちる・値が壊れる

reinterpret_cast やCスタイルで無関係な型のポインタにしてしまっているパターンです。キャストはコンパイラのチェックを黙らせるだけで、実態を合わせてはくれません。「エラーが消えたから直った」と思ったら危険信号で、そのキャストが本当に必要かを疑うべきです。

注意点

  • C++ではCスタイルの (型) を使わない。意図も残らないし、あとで検索できない
  • void* から元の型に戻すのはreinterpret_castではなくstatic_cast
  • dynamic_cast仮想関数がないと使えない。失敗時はポインタならnullptr、参照なら例外
  • 本物のconst変数をconst_castで書き換えるのは未定義動作。動いてしまうこともあるのが余計に怖い

まとめ

  • C++のキャストは、Cスタイルの (型)危険度で四つに分けたもの
  • static_cast…筋の通った変換。日常の9割はこれで足りる
  • dynamic_cast…継承のダウンキャスト。失敗をnullptrで受け取れる
  • reinterpret_cast…ビット列の解釈を変える。低レイヤー専用
  • const_cast…constを外す。古いAPIに渡すとき以外は使わない
  • 長い名前は面倒な規則ではなく、危ない箇所をあとで検索するための目印

最初は static_castdynamic_cast の2つだけ覚えておけば十分です。残りの2つは「使わないことを知っていればいい」キャストなので、まずは自分のコードの (int)(float)static_cast に書き換えるところから始めてみてください。

関連記事