C++では型変換は避けて通れませんが、やり方を誤ると未定義動作やバグの温床になります。
本記事では、初心者の方が安全に理解できるように、コンパイル時に確認できるstatic_cast
と、実行時に型を検査するdynamic_cast
を中心に、型変換の基礎から使い分けまで段階的に解説します。
C++の安全な型変換の基本
型変換が必要になる場面
プログラムでは、次のような場面で型変換が必要になります。
数値演算での型の統一、列挙値と整数の相互変換、C言語APIとのやり取りに伴うポインタ型の合わせ込み、そして継承関係にあるクラスを多態的に扱う際の基底型と派生型の相互変換です。
いずれも「何をどのタイミングで検査するか」を意識することが安全性につながります。
コンパイル時チェックと実行時チェック
型変換には大きく2種類のチェックがあります。
static_cast
はコンパイル時に可能な限りの整合性を確認しますが、ポインタの指す実体の「動的型」までは分かりません。
一方、dynamic_cast
は実行時にRTTI(Runtime Type Information)を用いて動的型を確認します。
そのため、ダウンキャストのように実体の型が重要な場面ではdynamic_cast
が役立ちます。
継承とポリモーフィズムでの型変換
継承関係でのアップキャスト(派生型から基底型へ)は常に安全です。
逆に、ダウンキャスト(基底型から派生型へ)は、ポインタが実際に指しているオブジェクトがその派生型である場合にのみ安全です。
ダウンキャストはdynamic_cast
で動的に確認するのが基本です。
static_castとdynamic_castの違い
次の表は両者の特徴をまとめたものです。
観点 | static_cast | dynamic_cast |
---|---|---|
チェックのタイミング | コンパイル時 | 実行時(動的型を検査) |
失敗時の挙動 | コンパイルエラーまたは未定義動作になり得る | ポインタ版はnullptr、参照版は例外(std::bad_cast) |
必要条件 | 変換が言語仕様で許可されていること | 基底型が多態的(少なくとも1つvirtual関数)であること |
主な用途 | 数値・列挙の明示変換、アップキャスト | 安全なダウンキャスト、交差キャスト |
速度 | ほぼゼロコスト | 実行時の型検査の分だけコストがかかる |
数値/列挙の変換 | 可能 | 不可 |
constの付け外し | 不可(const_castを使用) | 不可 |
未定義動作の回避 | 設計で担保が必要 | 失敗を検出できる設計が可能 |
static_castの使い方
構文と用途
static_cast<T>(expr)
は、数値・列挙・ポインタの静的な変換に使います。
特に曖昧さを避けるための「明示的な意図表示」として重要です。
#include <iostream>
int main() {
double d = 3.9;
// 小数点以下は切り捨てられる
int i = static_cast<int>(d);
char ch = 'A';
int code = static_cast<int>(ch); // 文字コードへ
std::cout << "d=" << d << ", i=" << i << '\n';
std::cout << "ch=" << ch << ", static_cast<int>(ch)=" << code << '\n';
}
d=3.9, i=3
ch=A, static_cast<int>(ch)=65
数値型と列挙型の安全な変換
縮小変換(より狭い型へ)は範囲チェックを行ってから変換します。
列挙型への変換は、定義済みの値かどうかを必ず確認してから行います。
#include <iostream>
#include <limits>
// スコープ付き列挙型は暗黙変換されないため安全性が高い
enum class Color : int { Red = 0, Green = 1, Blue = 2 };
// 列挙値に合法な整数かを確認するヘルパ
bool is_valid_color(int v) {
return v >= static_cast<int>(Color::Red)
&& v <= static_cast<int>(Color::Blue);
}
int main() {
// 縮小変換の安全確認
long long big = 1234567890123LL;
if (big >= std::numeric_limits<int>::min()
&& big <= std::numeric_limits<int>::max()) {
int n = static_cast<int>(big);
std::cout << "縮小変換OK: " << n << '\n';
} else {
std::cout << "縮小変換は範囲外のため行いません\n";
}
// 整数から列挙への変換は範囲確認が必須
int v1 = 2, v2 = 3;
if (is_valid_color(v1)) {
Color c1 = static_cast<Color>(v1);
std::cout << "Colorに変換成功: v1=" << v1 << '\n';
}
if (!is_valid_color(v2)) {
std::cout << "Colorに変換不可: v2=" << v2 << '\n';
}
// 列挙から基になる整数へ
int green_i = static_cast<int>(Color::Green);
std::cout << "Color::Green の整数値: " << green_i << '\n';
}
縮小変換は範囲外のため行いません
Colorに変換成功: v1=2
Colorに変換不可: v2=3
Color::Green の整数値: 1
継承のアップキャスト
アップキャスト(派生型→基底型)は常に安全で、通常は明示せずとも可能ですが、意図を示すためにstatic_cast
を使っても構いません。
#include <iostream>
#include <string>
struct Base {
virtual ~Base() = default;
virtual std::string name() const { return "Base"; }
};
struct Derived : Base {
std::string name() const override { return "Derived"; }
void only_derived() const { std::cout << "only_derived\n"; }
};
int main() {
Derived d;
// Derived* から Base* へのアップキャストは安全
Base* pb = static_cast<Base*>(&d);
// 仮想関数により実体に応じたメソッドが呼ばれる
std::cout << "pb->name(): " << pb->name() << '\n';
}
pb->name(): Derived
ダウンキャストの注意点
static_cast
でのダウンキャストはコンパイルは通りますが、実体の型が派生型でない場合は未定義動作になり得ます。
dynamic_cast
で確認できない状況でstatic_cast
のダウンキャストを使う設計は避けるべきです。
#include <iostream>
struct Base {
virtual ~Base() = default; // 多態性を持たせる
};
struct Derived : Base {
void hello() const { std::cout << "Derived::hello()\n"; }
};
int main() {
Derived d;
Base* pb1 = &d; // 実体はDerived
Derived* pd1 = static_cast<Derived*>(pb1); // ここは実体が合っているので結果的に安全
pd1->hello(); // OK
Base b;
Base* pb2 = &b; // 実体はBase
// 次はコンパイルは通るが安全ではない
Derived* pd2 = static_cast<Derived*>(pb2);
std::cout << "pd2 に対してメンバを呼び出すと未定義動作になる可能性があります\n";
// pd2->hello(); // 危険: 未定義動作になり得るためコメントアウト
}
Derived::hello()
pd2 に対してメンバを呼び出すと未定義動作になる可能性があります
dynamic_castの使い方
仮想関数とRTTIが必要な理由
dynamic_cast
は実行時にオブジェクトの動的型を照合します。
その仕組みはRTTIに依存するため、少なくとも1つの仮想関数を持つ「多態的」な基底クラスが必要です。
一般に、基底クラスに仮想デストラクタを持たせれば多態的になります。
ポインタ変換の失敗とnullptr
ポインタのdynamic_cast
は、失敗するとnullptr
を返します。
これにより安全に失敗を検出できます。
#include <iostream>
struct Base {
virtual ~Base() = default;
};
struct Derived : Base {
void hello() const { std::cout << "Derived::hello()\n"; }
};
int main() {
Derived d;
Base* pb1 = &d;
if (Derived* pd1 = dynamic_cast<Derived*>(pb1)) {
std::cout << "pb1 は Derived を指しています\n";
pd1->hello();
}
Base b;
Base* pb2 = &b;
if (Derived* pd2 = dynamic_cast<Derived*>(pb2)) {
pd2->hello();
} else {
std::cout << "pb2 は Derived ではないため nullptr が返りました\n";
}
}
pb1 は Derived を指しています
Derived::hello()
pb2 は Derived ではないため nullptr が返りました
参照変換の失敗と例外
参照のdynamic_cast
は、失敗するとstd::bad_cast
例外を投げます。
例外を使って確実にハンドリングできます。
#include <iostream>
#include <typeinfo>
struct Base { virtual ~Base() = default; };
struct Derived : Base {};
int main() {
Base b;
Base& rb = b;
try {
// 失敗すると std::bad_cast が送出される
Derived& dr = dynamic_cast<Derived&>(rb);
(void)dr; // 未使用警告抑止
std::cout << "参照変換に成功しました\n";
} catch (const std::bad_cast& e) {
std::cout << "参照変換は失敗し、例外を捕捉: " << e.what() << '\n';
}
}
参照変換は失敗し、例外を捕捉: std::bad_cast
多態性を生かしたダウンキャスト
通常は仮想関数で多態性を発揮すべきですが、例外的に派生型特有の追加情報を使いたい場合にdynamic_cast
が役立ちます。
#include <iostream>
#include <vector>
struct Animal {
virtual ~Animal() = default;
virtual void speak() const = 0; // 多態的な共通インターフェース
};
struct Dog : Animal {
void speak() const override { std::cout << "Woof!\n"; }
void wag() const { std::cout << "Dog is wagging tail.\n"; }
};
struct Cat : Animal {
void speak() const override { std::cout << "Meow!\n"; }
void purr() const { std::cout << "Cat is purring.\n"; }
};
int main() {
std::vector<Animal*> zoo;
zoo.push_back(new Dog{});
zoo.push_back(new Cat{});
zoo.push_back(new Dog{});
for (Animal* a : zoo) {
a->speak(); // まずは仮想関数で共通処理
if (Cat* c = dynamic_cast<Cat*>(a)) {
// Catにだけ追加で特別な処理を行う
std::cout << "Cat専用の処理: ";
c->purr();
}
}
for (Animal* a : zoo) delete a;
}
Woof!
Meow!
Cat専用の処理: Cat is purring.
Woof!
static_castとdynamic_castの使い分け
安全性が最優先ならdynamic_cast
実体の型が確信できない、もしくは外部入力や複数の派生型が混在する状況ではdynamic_cast
を使います。
失敗を検知できるため、早期に誤りに気付けます。
参照が必要な場合は例外で、ポインタならnullptr
の戻りで分岐します。
性能重視ならstatic_cast
明確に型が分かっているホットパスではstatic_cast
が有利です。
ただし「本当に型が合っている」ことをプログラムロジックで保証しましょう。
例えば、ループの外側で一度だけdynamic_cast
で確認し、以降は結果を使い回すと過剰なチェックを避けられます。
#include <iostream>
struct Base { virtual ~Base() = default; };
struct Derived : Base { void heavy() const { /* 重い処理の想定 */ } };
int main() {
Derived d;
Base* p = &d;
// 1回だけdynamic_castで検査し、以降は派生型ポインタを直接使う
if (Derived* pd = dynamic_cast<Derived*>(p)) {
for (int i = 0; i < 3; ++i) {
pd->heavy(); // ループ内で追加のキャスト不要
}
std::cout << "チェック済みのためループ内では追加のキャスト不要\n";
} else {
std::cout << "型が違うため処理しません\n";
}
}
チェック済みのためループ内では追加のキャスト不要
設計により、動的チェックを明示的に行ってからstatic_cast
を使うことも可能です。
例えば基底に仮想関数で「種別(kind)」を返すAPIを設け、該当種別のときにのみstatic_cast
を行うと、意図がコードに表れ保守性が高まります。
#include <iostream>
struct Base {
enum class Kind { Base, Derived };
virtual ~Base() = default;
virtual Kind kind() const { return Kind::Base; }
};
struct Derived : Base {
Kind kind() const override { return Kind::Derived; }
void work() const { std::cout << "Derived::work()\n"; }
};
int main() {
Derived d;
Base* p = &d;
if (p->kind() == Base::Kind::Derived) {
// 種別チェック後のstatic_castは意図が明確
auto* pd = static_cast<Derived*>(p);
pd->work();
}
}
Derived::work()
ダウンキャストを減らす設計
そもそもダウンキャストを必要としない設計を目指すのが最善です。
多くの場合、基底クラスに適切な仮想関数を用意しておけば、利用側が型分岐する必要はありません。
新たな派生型が増えても、仮想関数の実装を追加するだけで拡張できます。
#include <iostream>
#include <vector>
struct Shape {
virtual ~Shape() = default;
virtual void draw() const = 0; // これを用意すれば利用側はダウンキャスト不要
};
struct Circle : Shape {
void draw() const override { std::cout << "Draw Circle\n"; }
};
struct Rectangle : Shape {
void draw() const override { std::cout << "Draw Rectangle\n"; }
};
int main() {
std::vector<Shape*> canvas { new Circle{}, new Rectangle{} };
for (const Shape* s : canvas) {
s->draw(); // 型ごとの適切な動作が選ばれる
}
for (Shape* s : canvas) delete s;
}
Draw Circle
Draw Rectangle
変換結果のチェック方法
dynamic_cast
の典型的なチェック方法は次の2通りです。
ポインタ変換なら戻り値がnullptr
かどうか、参照変換なら例外の有無です。
コードの見通しを良くするため、if文内で結果を受け取るイディオムを活用すると読みやすくなります。
#include <iostream>
#include <typeinfo>
struct Base { virtual ~Base() = default; };
struct Derived : Base {};
void check_with_pointer(Base* p) {
if (Derived* d = dynamic_cast<Derived*>(p)) {
std::cout << "pointer: Derived\n";
} else {
std::cout << "pointer: not Derived\n";
}
}
void check_with_ref(Base& b) {
try {
(void)dynamic_cast<Derived&>(b);
std::cout << "reference: Derived\n";
} catch (const std::bad_cast&) {
std::cout << "reference: not Derived\n";
}
}
int main() {
Derived d;
Base b;
check_with_pointer(&d);
check_with_pointer(&b);
check_with_ref(d);
check_with_ref(b);
}
pointer: Derived
pointer: not Derived
reference: Derived
reference: not Derived
まとめ
本記事では、C++における安全な型変換の基本として、static_cast
とdynamic_cast
の役割と使い分けを解説しました。
要点は次の通りです。
数値や列挙の明示的な変換、アップキャストにはstatic_cast
を使い、動的な型検査が必要なダウンキャストにはdynamic_cast
を用います。
dynamic_cast
はポインタで失敗時にnullptr
、参照でstd::bad_cast
となるため、確実に失敗を検出できます。
性能面が気になる場面では、型が確実に分かっていることを設計で担保し、チェックの回数を最小限に抑える工夫を行います。
最良の対策は、仮想関数などを活用してダウンキャスト自体を不要にする設計です。
これらの原則を守ることで、読みやすく安全で拡張しやすいC++コードを書けるようになります。