Modern C++編【言語解説】 第9章 型の変換

先頭へ戻る

この章の概要

この章の概要です。

C++ のキャスト

C++ でも、C言語と同様に ( ) によるキャストが使えますが、 C++ には新たに以下の4種類のキャストが追加されています。

これらの新しいキャストはいずれも、次のような構文で使用します。

キャストの名称<キャスト後の型>(式);

C言語形式のキャストと比べて、長ったらしくなって面倒ですが、 C++ のキャストは、キャストの目的が明確になる利点があります。 4つのキャストにはそれぞれ異なる用途があり、合致しないキャストを選ぶとコンパイルエラーになります。
基本的に、C++ のキャストを使うようにすべきですし、 更に言えば、極力キャストすることが無いように、プログラムを設計することが望ましいと言えます。

この後、それぞれのキャストについて説明しますが、 dynamic_cast に関してだけは、この章までの内容だけでは解説できないので、 第37章で改めて取り上げます。

static_cast

static_cast で行える変換は、例えば以下のようなものです。

4つ目についてはこの章で後で取り上げます。

以下に、static_cast の使い方の例を挙げます。

int main()
{
    enum E {
        E1
    };

    int n1 = static_cast<int>(12.3);  // 警告を黙らせるキャスト
    short n2 = n1;                    // 警告を黙らせるキャスト

    E e = static_cast<E>(n1);         // キャスト必須
    n1 = e;                           // キャスト不要

    void* pv = &n1;                   // キャスト不要
    int* p = static_cast<int*>(pv);   // キャスト必須
}

実行結果:




ポインタ型の変換についてですが、void* でないポインタ間での変換は static_cast では行えないので、 reinterpret_cast を使います。 まとめると、次のようになります。

const_cast

const_cast は、const修飾子および volatile修飾子を外すキャストです。

int main()
{
    int num = 100;
    const int* cp = &num;
    int* p = const_cast<int*>(cp);

    volatile int* vp = &num;
    p = const_cast<int*>(vp);
}

実行結果:




まず、基本的に、自分で書いた C++ のプログラムの中で const_cast が登場するのは、 設計的な欠陥の可能性があることを認識して下さい。 const_cast が用意されている理由の1つは、 const や volatile に対応していなかった古いC言語のプログラムを利用しなければならないとき等があるからです。

非constメンバ関数と、constメンバ関数とのオーバーロードを行う場合に、 その実装を共通化する目的で const_cast を使うことがあります。 これは const_cast の使い道として妥当なものです(第10章参照

const_cast によって const修飾子を外せるという事実と、外した後に値を書き換えても安全かどうかとは別問題です。 元々、値を書き換えないことを明示するために const修飾子を付けているのですから、 書き換えを行うことは、当然危険な可能性があります。

reinterpret_cast

reinterpret_cast は、 整数型とポインタ型との相互変換や、ポインタ型を異なるポインタ型への変換に使用できます。

int main()
{
    int a = 10;
    int* pn = &num;
    short* ps = reinterpret_cast<short*>(pn);

    unsigned long long address = reinterpret_cast<unsigned long long>(pn);
}

reinterpret_cast は、変換前後でビットの並びを変えず、型の解釈だけを変えるという風に捉えることができます。 少なくとも、A型からB型へ変換した後、再びA型に変換し直したときに、完全に元に戻ることは保証されています。

const_cast と同様、reinterpret_cast も極力使用を避けるべきです。 reinterpret_cast は、C言語で書かれたコールバック関数(C言語編第37章)のように、引数を void*型や整数型で受け取り、 関数内で本来の型に戻さなければならないような場面で使用します。


継承関係にあるクラスを指すポインタにおいて、適切にダウンキャストを行うには、reinterpret_cast を使ってはならず、 static_cast か dynamic_cast を使わなくてはなりません。 一般に、継承関係にあるクラスのポインタを変換する際には、 メモリアドレスに調整を加える必要があるからです。 reinterpret_cast は "ビットの並びを変えず、型の解釈だけを変える" ので、 reinterpret_cast ではメモリアドレスの調整が起こりませんから、 正しくキャストできる保証がありません(第37章参照)。

型変換演算子

クラスに、型変換演算子を定義しておくと、これを定義したクラスのオブジェクトを、別の型へ変換できるようになります。
型変換演算子は、メンバ関数であると考えれば良いです。 引数は無く、戻り値で変換後の値を返します。

型変換演算子は、名前の付け方が特殊で、operatorキーワードを使って「operator 変換後の型名」とします。 また、戻り値の型は名前から判断できるため、記述を省略します(しなければなりません)。 なお、変換後の型として、配列型や関数型を使うことはできません。

例えば、ファイルを扱うクラスを作るとします。

#include <cstdio>
#include <iostream>

class File {
public:
    File(const char* fileName);
    ~File();

    bool IsOpen() const;

private:
    std::FILE*      mFp;
};

File::File(const char* fileName) :
    mFp(std::fopen(fileName, "r"))
{
}

File::~File()
{
    std::fclose(mFp);
}

bool File::IsOpen() const
{
    return mFp != nullptr;
}

int main()
{
    File file("test.bin");
    if (!file.IsOpen()) {
        std::cout << "error" << std::endl;
    }

    // 以下省略
}

IsOpen() は、ファイルが開かれているかどうかを確認するメンバ関数ですが、これを型変換演算子で実装することができます。以下に、関係がある部分だけを載せます。

class File {
public:
    operator bool() const;

private:
    std::FILE*      mFp;
};

File::operator bool() const
{
    return mFp != nullptr;
}

int main()
{
    File file("test.bin");
    if (!file) {
        std::cout << "error" << std::endl;
    }

    // 以下省略
}

operator bool() を定義したことで、File型のオブジェクトを bool型に変換することができるようになりました。 そのため、「if (!file)」のような使い方ができます。 この場合、まず File型の file が bool型に変換され、それを「!」で否定していることになります。

型変換演算子はうまく使うと非常に便利ですが、暗黙的に変換が行われるため、意図しない変換をしてしまうことがあります。例えば、引数の型が bool の関数に、File型のオブジェクトを渡すことができてしまう訳ですが、これは意図した処理でしょうか?

void func(bool b);

int main()
{
    File file("test.bin");
    func(file);  // 意図したこと?
}

このような使われ方を避けるためには、型変換演算子の宣言に、explicitキーワードを付加します。こうすると、暗黙的な型変換には型変換演算子が使われなくなります。必要なときは、static_cast を使うなどして明示的に行います。

class File {
public:
    explicit operator bool() const;
};

void func(bool b);

int main()
{
    File file("test.bin");
    func(file);                     // コンパイルエラー
    func(static_cast<bool>(file));  // OK

    if (!file) {}                   // これは OK
}

あるいは、オブジェクトを直接初期化(第7章)する際には、explicit の付いた型変換演算子が使われます。

explicit を付けても、条件式での使用を妨げないことに注意して下さい。

なお、型変換演算子の利用をやめて、「ToInt」「ToString」のような明確な名前を付けたメンバ関数を用意するのも良い選択です。

型変換コンストラクタ

実引数を1つだけ指定して呼び出すことができるコンストラクタは、 型変換コンストラクタと呼ばれます。 型変換コンストラクタは、そのクラスとは異なる型から、クラスをインスタンス化するものです。

引数が2つ以上あるコンストラクタでも、2つ目以降の引数がデフォルト値(第10章)を持つならば、 やはり型変換コンストラクタと呼びます。

型変換演算子のところで使った Fileクラスの例を見てみると、既に、型変換コンストラクタがあります。

class File {
public:
    File(const char* fileName);
};

const char*型の引数を1つだけ指定して呼び出すことができますから、これは型変換コンストラクタとして機能します。これがあると、コピー初期化(第7章)の構文であれば、次のようにインスタンス化することができます。

File file = "test.bin";  // コピー初期化なら OK
File file("test.bin");   // 直接初期化ではコンパイルエラー

コピー初期化の場合は、ユーザー定義の型変換処理を適用しようするため、型変換コンストラクタが機能します。直接初期化では、適用しないためコンパイルエラーになります。

型変換コンストラクタがあると、次のような意図しないであろう使い方が可能になってしまいます。

void func(File f);

func("Hello");   // 意図通り?

仮引数が File型の関数に、"Hello" という文字列リテラルを渡そうとしています。恐らく、意図したものでは無いであろう使い方ですが、const char*型からインスタンス化を行う型変換コンストラクタがあるため、コンパイルに成功します。この場面で、型変換コンストラクタが機能してしまうのは、引数や戻り値を使って行う初期化はコピー初期化だからです(第7章)。

このように、型変換コンストラクタが定義されていると、暗黙的な型変換が行われ、コードが分かりづらくなることがあります。

型変換演算子の場合と同様で、暗黙的に使われても構わないかどうか、よく検討して下さい。 暗黙的な使用は望ましくないが、型変換自体は有用であるのなら、暗黙的には機能しないようにすることができます。そのためには、型変換コンストラクタの宣言に、explicitキーワードを付加します。

class File {
public:
    explicit File(const char* fileName);
};

void func(File f);

int main()
{
    File file = "test.bin";  // コンパイルエラー
    File file("test.bin");   // OK

    func("Hello");                      // コンパイルエラー
    func(static_cast<File>("Hello"));   // OK
    func(File("Hello"));                // OK
}

explicit の付いた型変換コンストラクタは、直接初期化のときにだけ使用され、コピー初期化では使用されません。

実引数を1つ指定するだけで呼び出せるコンストラクタを作る際、それが、型変換コンストラクタを作っているというつもりで無いのなら(たまたま引数が1個になるというだけならば)、常に explicit を付けるようにした方が良いです。


練習問題

問題① 次の各変換は、static_cast、const_cast、reinterpret_cast、C言語形式のキャスト、キャストは不要、のいずれで行えるか答えて下さい。

  1. long → short
  2. bool → int
  3. int* → int
  4. const char* → char*
  5. struct MyData* → void*
  6. struct MyData* → const struct MyData*
  7. void* → struct MyData*
  8. const float* → int*
  9. bool (*f)(int) → bool (*f)(const char*)


解答ページはこちら

参考リンク

更新履歴

'2017/12/9 「型変換演算子」「型変換コンストラクタ」の解説を、コピー初期化や直接初期化の話題を絡めて行うように修正。

'2017/7/27 新規作成。





前の章へ(第8章 クラステンプレート)

次の章へ(第10章 関数オーバーロードとデフォルト引数)

Modern C++編のトップページへ

Programming Place Plus のトップページへ


このエントリーをはてなブックマークに追加
rss1.0 取得ボタン RSS