C++では型変換の自由度が高いため、便利な一方でバグや未定義動作を招きやすい側面があります。
本記事では安全に明示的な型変換を行うためのstatic_cast
とdynamic_cast
の基礎を、初心者の方にも分かるように丁寧に説明します。
モダンな作法(例: nullptr
の使用、using namespace std;
を避ける)にも配慮してコード例を示します。
C++の安全な型変換
なぜ明示的キャストが必要か
C++では暗黙の型変換が数多く存在しますが、意図が曖昧な変換はバグの温床です。
たとえばポインタのダウンキャストや数値の丸めを暗黙に行うと、データ喪失や未定義動作につながります。
そこで明示的なキャストを使い、開発者が意図をコードに刻み込みます。
特にC
言語スタイルのキャスト(T)x
や(T*)p
は、複数の意味を一括で許容するため危険です。
const
を外したり、ビット解釈を変えたりと、望まない強い変換まで通ってしまいます。
モダンC++ではstatic_cast
やdynamic_cast
など用途ごとの安全なキャストを用いるのが基本です。
static_castとdynamic_castの概要
static_cast
はコンパイル時に意味が決まる一般的な変換に適し、dynamic_cast
は実行時に型を確認するダウンキャストに適します。
アップキャストはstatic_cast
、ポリモーフィズムを伴うダウンキャストはdynamic_cast
が原則です。
以下に特徴を整理します。
項目 | static_cast | dynamic_cast |
---|---|---|
主用途 | 数値/列挙/ポインタの安全な変換、アップキャスト | ポリモーフィック型のダウンキャスト |
実行時チェック | なし | あり(RTTIによる検査) |
失敗時 | コンパイルは通るが実行時未定義動作の恐れ | ポインタはnullptr 、参照はstd::bad_cast 例外 |
前提 | 特になし | 基底型がポリモーフィック(少なくとも1つ仮想関数) |
速度感 | 速い(ゼロオーバーヘッドが多い) | 遅い(実行時チェックの分だけコスト) |
ダウンキャストにstatic_cast
を使うのは危険で、未定義動作を引き起こす可能性があります。
static_castの基本と使い方
使える場面(数値/列挙/アップキャスト)
static_cast
は次のような場面で有効です。
いずれもコンパイル時に意味が確定し、実行時チェックは行いません。
- 数値の変換(例:
double
からint
) - 列挙型と整数の相互変換(明示的に行うのが安全)
- 派生→基底へのアップキャスト(ポリモーフィズムの有無を問わない)
使い方の例(ポインタ/参照)
以下のコードでは、数値/列挙、そしてクラスのアップキャストにおけるstatic_cast
を示します。
using namespace std;
は可読性や名前衝突の観点から避け、std::
接頭辞を使います。
またヌルポインタはNULL
ではなくnullptr
を使います。
#include <iostream>
#include <string>
class Base {
public:
// 仮想関数がなくてもアップキャストは可能
virtual ~Base() = default; // 仮想デストラクタは拡張性のため推奨
virtual void speak() const { std::cout << "Base::speak\n"; }
};
class Derived : public Base {
public:
void speak() const override { std::cout << "Derived::speak\n"; }
void onlyDerived() const { std::cout << "Derived::onlyDerived\n"; }
};
enum class Color { Red = 1, Green = 2 };
int main() {
// 数値の変換
double pi = 3.14159;
int n = static_cast<int>(pi); // 小数部は切り捨て
std::cout << "n = " << n << "\n";
// 列挙と整数
Color c = Color::Green;
int code = static_cast<int>(c); // 列挙値を明示的に整数へ
std::cout << "code = " << code << "\n";
// アップキャスト(ポインタ)
Derived d;
Base* bp = static_cast<Base*>(&d); // OK: Derived* -> Base*
bp->speak(); // 仮想呼び出しでDerived::speakが呼ばれる
// アップキャスト(参照)
Base& br = static_cast<Base&>(d); // OK: Derived& -> Base&
br.speak();
// ヌルポインタはnullptrを使用
Base* nullp = nullptr;
if (!nullp) {
std::cout << "nullp is nullptr\n";
}
}
n = 3
code = 2
Derived::speak
Derived::speak
nullp is nullptr
注意点(ダウンキャストには使わない)
ダウンキャスト(基底→派生)にstatic_cast
を使うのは危険です。
型が本当に派生型インスタンスである保証がなければ、未定義動作になります。
以下はstatic_cast
が通ってしまうものの、呼び出しは未定義動作の例です。
実行してはいけません。
#include <iostream>
class Base { public: virtual ~Base() = default; };
class Derived : public Base { public: void onlyDerived() {} };
int main() {
Base b; // 実体はBase
Base* bp = &b;
// コンパイルは通るが、実体はDerivedではない
Derived* dp = static_cast<Derived*>(bp); // 危険: 未定義動作の種
// dp->onlyDerived(); // 実行時に未定義動作。呼び出さないこと。
}
ダウンキャストはdynamic_cast
で実行時確認を入れて安全に行います。
#include <iostream>
class Base { public: virtual ~Base() = default; };
class Derived : public Base { public: void onlyDerived() {} };
int main() {
Base b; // 実体はBase
Base* bp = &b;
// 実体がDerivedかどうかを安全に確認
if (Derived* dp = dynamic_cast<Derived*>(bp)) {
dp->onlyDerived();
} else {
std::cout << "bpはDerivedではありません\n";
}
}
static_cast
は実行時の型を確認しないため危険ですが、dynamic_cast
を使えば安全にダウンキャストできます。
キャスト失敗時はnullptr
が返るため、条件分岐で安全に処理できます。
dynamic_castの基本と使い方
使う場面(ポリモーフィズムのダウンキャスト)
dynamic_cast
は実行時型情報(RTTI)に基づいて、基底ポインタ/参照から真の派生型を確認します。
継承階層でのダウンキャストやクロスキャスト(多重継承)で使われます。
前提条件(基底に仮想関数があること)
基底クラスがポリモーフィック型(少なくとも1つ仮想関数を持つ)である必要があります。
そうでないとdynamic_cast
は使えません。
// これはOK: Baseが仮想関数(仮想デストラクタでも可)を持つ
class Base { public: virtual ~Base() = default; };
class Derived : public Base {};
// これはNG: 仮想関数がないとダウンキャストでdynamic_castは使えない
class NonPolyBase {}; // 非ポリモーフィック
class NonPolyDerived : public NonPolyBase {};
// NonPolyBase* p = ...;
// dynamic_cast<NonPolyDerived*>(p); // エラー: source type is not polymorphic
失敗時の挙動(nullptr/bad_cast)
dynamic_cast
は失敗時の挙動が2通りあります。
- ポインタの変換: 失敗すると
nullptr
を返す - 参照の変換: 失敗すると
std::bad_cast
例外を投げる
使い方の例(ポインタ/参照)
次の例ではポインタと参照の両方でdynamic_cast
を使い、成功/失敗時の挙動を確認します。
#include <iostream>
#include <typeinfo> // std::bad_cast
class Base {
public:
virtual ~Base() = default; // ポリモーフィックにする
virtual void speak() const { std::cout << "Base::speak\n"; }
};
class Derived : public Base {
public:
void speak() const override { std::cout << "Derived::speak\n"; }
void onlyDerived() const { std::cout << "Derived::onlyDerived\n"; }
};
int main() {
Derived d;
Base b;
// ポインタでのdynamic_cast
Base* pb1 = &d; // 実体はDerived
Base* pb2 = &b; // 実体はBase
if (Derived* pd1 = dynamic_cast<Derived*>(pb1)) {
std::cout << "pb1 -> Derived* OK: ";
pd1->onlyDerived();
} else {
std::cout << "pb1 -> Derived* FAIL\n";
}
if (Derived* pd2 = dynamic_cast<Derived*>(pb2)) {
std::cout << "pb2 -> Derived* OK\n";
} else {
std::cout << "pb2 -> Derived* FAIL (nullptr)\n";
}
// 参照でのdynamic_cast(失敗時は例外)
Base& rb1 = d; // 実体はDerived
Base& rb2 = b; // 実体はBase
try {
Derived& rd1 = dynamic_cast<Derived&>(rb1); // 成功
std::cout << "rb1 -> Derived& OK: ";
rd1.onlyDerived();
} catch (const std::bad_cast& e) {
std::cout << "rb1 -> Derived& FAIL: " << e.what() << "\n";
}
try {
Derived& rd2 = dynamic_cast<Derived&>(rb2); // 失敗して例外
(void)rd2;
} catch (const std::bad_cast& e) {
std::cout << "rb2 -> Derived& FAIL: " << e.what() << "\n";
}
}
pb1 -> Derived* OK: Derived::onlyDerived
pb2 -> Derived* FAIL (nullptr)
rb1 -> Derived& OK: Derived::onlyDerived
rb2 -> Derived& FAIL: std::bad_cast
失敗を安全に検出できるのがdynamic_cast
の最大の利点です。
static_castとdynamic_castの使い分け
基本ルール(アップキャスト/ダウンキャスト)
使い分けは次の原則を守ると安全です。
- アップキャスト(Base←Derived)は
static_cast
で十分です。実行時チェックは不要で、オーバーヘッドもありません。 - ダウンキャスト(Derived←Base)は
dynamic_cast
で確認します。実体が派生型でない可能性があるならnullptr
やstd::bad_cast
で検出できます。
なおconst_cast
はconst性を外す専用、reinterpret_cast
はビット解釈の再解釈であり、いずれも本記事の主題である「安全な型変換」からは外れます。
安易に使わないでください。
エラーを防ぐチェックリスト(初心者向け)
- 基底クラスに
virtual
な関数(最低でも仮想デストラクタ)はありますか。 - ダウンキャストは
dynamic_cast
でnullptr
チェック(ポインタ)やtry-catch
(参照)をしていますか。 static_cast
はアップキャストや数値/列挙など、意味が明確な場面に限定していますか。using namespace std;
を避け、nullptr
を使っていますか。
迷ったらdynamic_castで確認する
性能要件が緩い箇所ではまずdynamic_cast
で安全を優先し、成立が確実でホットパスの場合だけ設計を見直すのが実践的です。
「ほんとうにその型なのか」を常に疑い、実行時に確認できる形にしましょう。
設計面では、virtual
関数やダブルディスパッチ、訪問者パターンなど、ダウンキャスト自体を減らす発想も有効です。
まとめ
型変換は「明示」と「安全」を最優先にすべきです。
アップキャストや数値/列挙にはstatic_cast
、ポリモーフィズムを伴うダウンキャストにはdynamic_cast
を使い、失敗時の検出を活用します。
基底型にはvirtual
な関数を持たせ、コードではnullptr
を使い、using namespace std;
を避けるなど、モダンな作法も合わせて守ると良いでしょう。
最終的にはダウンキャストを必要最小限に設計することが、堅牢なプログラムへの近道です。