閉じる

C++の安全な型変換のやり方: static_castとdynamic_cast入門

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_castdynamic_cast
チェックのタイミングコンパイル時実行時(動的型を検査)
失敗時の挙動コンパイルエラーまたは未定義動作になり得るポインタ版はnullptr、参照版は例外(std::bad_cast)
必要条件変換が言語仕様で許可されていること基底型が多態的(少なくとも1つvirtual関数)であること
主な用途数値・列挙の明示変換、アップキャスト安全なダウンキャスト、交差キャスト
速度ほぼゼロコスト実行時の型検査の分だけコストがかかる
数値/列挙の変換可能不可
constの付け外し不可(const_castを使用)不可
未定義動作の回避設計で担保が必要失敗を検出できる設計が可能

static_castの使い方

構文と用途

static_cast<T>(expr)は、数値・列挙・ポインタの静的な変換に使います。

特に曖昧さを避けるための「明示的な意図表示」として重要です。

C++
#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

数値型と列挙型の安全な変換

縮小変換(より狭い型へ)は範囲チェックを行ってから変換します。

列挙型への変換は、定義済みの値かどうかを必ず確認してから行います。

C++
#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を使っても構いません。

C++
#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のダウンキャストを使う設計は避けるべきです。

C++
#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を返します。

これにより安全に失敗を検出できます。

C++
#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例外を投げます。

例外を使って確実にハンドリングできます。

C++
#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が役立ちます。

C++
#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で確認し、以降は結果を使い回すと過剰なチェックを避けられます。

C++
#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を行うと、意図がコードに表れ保守性が高まります。

C++
#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()

ダウンキャストを減らす設計

そもそもダウンキャストを必要としない設計を目指すのが最善です。

多くの場合、基底クラスに適切な仮想関数を用意しておけば、利用側が型分岐する必要はありません。

新たな派生型が増えても、仮想関数の実装を追加するだけで拡張できます。

C++
#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文内で結果を受け取るイディオムを活用すると読みやすくなります。

C++
#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_castdynamic_castの役割と使い分けを解説しました。

要点は次の通りです。

数値や列挙の明示的な変換、アップキャストにはstatic_castを使い、動的な型検査が必要なダウンキャストにはdynamic_castを用います。

dynamic_castはポインタで失敗時にnullptr、参照でstd::bad_castとなるため、確実に失敗を検出できます。

性能面が気になる場面では、型が確実に分かっていることを設計で担保し、チェックの回数を最小限に抑える工夫を行います。

最良の対策は、仮想関数などを活用してダウンキャスト自体を不要にする設計です。

これらの原則を守ることで、読みやすく安全で拡張しやすいC++コードを書けるようになります。

この記事を書いた人
エーテリア編集部
エーテリア編集部

C++をこれから学ぶ方に向けて、基礎的な文法や標準ライブラリの使い方を紹介します。モダンな書き方も初心者に合わせてやさしく説明しています。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!