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

トップページModern C++編

Modern C++編は作りかけで、更新が停止しています。代わりに、C++14 をベースにして、その他の方針についても見直しを行った、新C++編を作成しています。
Modern C++編は削除される予定です。

この章の概要 🔗

この章の概要です。


C++ のキャスト 🔗

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

この章では dynamic_cast 以外の3つについて解説します。

C言語形式のキャストには、意図が明確にならない欠点があります。たとえば (char*)p というコードは、「int* から char*」へのキャストかもしれないし、「const char* から char*」へのキャストかもしれないし、他の何かかもしれません。

C++ のキャストは、意図を明確にする効果があります。そして、その意図に合わないキャスト構文を選択した場合、コンパイルエラーを起こして、プログラマーに間違いを教えてくれます。

【上級】dynamic_cast だけは、間違いを実行時に検出します。

また、C++ のキャスト構文には、先ほどのリストで挙げたような名前が付いていますから、ソースファイル内でよく目立ち、キャストを行っている箇所を検索することが容易です。

C++ では、C言語形式のキャストを使うことはやめて、新しい方法を選ぶべきです。C++ のキャストの方が機能が限定的ですから、1つのキャストだけで賄えないときには、複数のキャストを組み合わせます。

C++ の新しいキャストの構文は、4つとも次の形です。

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

式の評価は行われます。

static_cast 🔗

static_cast がもっとも一般的なキャストです。

static_cast<キャスト後の型>();

適切なキャストがどれか分からなければ、まず試すべきは static_cast です。このキャストが不適切であれば、コンパイルエラーになります。たとえば、ポインタや参照から const や volatile を取り除くようなキャストは拒否されます

参照は、第12章で解説します。

int main()
{
    int n = 100;
    const int* cp = &n;
    volatile int* vp = &n;

    int* p;
    p = static_cast<int*>(cp);  // コンパイルエラー
    p = static_cast<int*>(vp);  // コンパイルエラー
}

暗黙の型変換によって情報が失われる可能性があるとき、コンパイラは警告を出すことがありますが、これを(プログラマーの責任で)黙らせるためことが可能です。

int main()
{
    int n = 100;

    signed char s = static_cast<signed char>(n);
    float f = static_cast<float>(n);
}

整数型から列挙型への変換が行えます。逆方向は暗黙的に変換できます。

整数型から列挙型への変換は、C言語では暗黙的に可能でした(C言語編第50章)。

#include <iostream>

int main()
{
    enum Color {
        RED,
        GREEN,
        BLUE,
    };

    Color color = static_cast<Color>(1);

    std::cout << color << std::endl;
}

実行結果:

100

voidポインタから、それ以外の型のポインタへの変換が行えます。逆方向は暗黙的に変換できます。そのポインタが保持しているメモリアドレスは変化しません。

voidポインタから、それ以外の型のポインタへの変換は、C言語では暗黙的に可能でした(C言語編第34章)。

#include <iostream>

int main()
{
    int n = 100;

    void* vp = &n;
    int* np = static_cast<int*>(vp);

    std::cout << *np << std::endl;
}

実行結果:

100

voidポインタ以外のポインタ型同士での変換は行えません。たとえば、int* と char* との間での変換はできません。

int main()
{
    int n = 100;

    int* pn = &n;
    char* pc = pn;  // コンパイルエラー
}

【上級】static_cast のそのほかの用途に、派生クラスへのダウンキャストがあります。

reinterpret_cast 🔗

reinterpret_cast は、式の値を変えずに、型だけを変えるようなキャストです。

reinterpret_cast<キャスト後の型>();

たとえば整数と浮動小数点数とでは表現方法が異なるように、型によって値の表現形式が異なることがありますが、reinterpret_cast は、そういったことに感知しません。式の値が、変換後の型においても適切なものかどうかは、プログラマーが責任を持たなければなりません。その意味で危険性があります。

また、多くの部分が実装に依存した仕様になっており、移植性が非常に低いキャストです。

整数型や列挙型と、ポインタ型との相互変換が行えます。型の大きさは実装依存ですから、情報を失わないために、変換後の型の大きさが十分であることをプログラマーが保証しなければなりません。次のサンプルプログラムでは、unsigned long型が十分な大きさを持っていることを前提としています。

#include <iostream>

int main()
{
    int a = 100;

    int* p = &a;
    std::cout << p << std::endl;

    unsigned long n = reinterpret_cast<unsigned long>(p);
    std::cout << std::hex << n << std::endl;

    p = reinterpret_cast<int*>(n);
    std::cout << p << std::endl;
}

実行結果:

00CFF9F8
cff9f8
00CFF9F8

異なるポインタ型同士や、異なる参照型同士の変換が行えます。しかし、変換後のポインタを間接参照した結果は実装依存です。保証されているのは、変換後の型から、変換前の型へきちんと戻せることだけです。また、const や volatile を外すことはできません。

参照は、第12章で解説します。

#include <iostream>

int main()
{
    int n = 100;

    int* pn = &n;
    short* ps = reinterpret_cast<short*>(pn);

    std::cout << *ps << std::endl;  // *ps の結果は保証なし
}

異なる関数ポインタ型同士の変換が行えます。しかし、変換後の関数ポインタを経由した関数呼び出しが、正しく機能するかどうかは実装依存です。保証されているのは、変換後の型から、変換前の型へきちんと戻せることだけです。

#include <iostream>

void f(int v)
{
    std::cout << v << std::endl;
}

int main()
{
    typedef void (*func_t)(short);

    func_t func = reinterpret_cast<func_t>(f);
    func(100);  // 保証なし
}

【上級】基底クラスを指すポインタから、派生クラスを指すポインタにダウンキャストを行うには、reinterpret_cast を使ってはならず、static_cast か dynamic_cast を使わなくてはなりません。これはメモリアドレスに調整を加えなければならないことがあるためです。冒頭で、reinterpret_cast を “式の値を変えずに” と書いたように、reinterpret_cast ではメモリアドレスの調整が起こらないので、正しくキャストできません。

const_cast 🔗

const_cast は、const修飾子や volatile修飾子に関するキャストです。

const_cast<キャスト後の型>();

プログラムの中で const_cast が登場するのは、設計的な欠陥の可能性があります。何らかの必要性をもって付加されているはずの const や volatile を外す行為には、常に危険が伴います。

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

ポインタや参照に付いている const や volatile を外せます。

参照は、第12章で解説します。

#include <iostream>

int main()
{
    int n = 100;

    const int* cp = &n;
    int* p = const_cast<int*>(cp);

    std::cout << *p << std::endl;
}

実行結果:

100

const や volatile を付け加えることもできます。これは、暗黙的にも行われることですし、明示的にしなければならない場面では static_cast を使うこともできます。

【上級】たとえば、あるメンバ関数が constメンバ関数と非constメンバ関数とでオーバーロードされているときに、強制的に constメンバ関数の方を呼び出したいときに、明示的に const を付加する必要性があります(第10章参照)。

#include <iostream>

int main()
{
    int n = 100;

    int* p = &n;
    const int* cp = const_cast<const int*>(p);  // 暗黙的にも、static_cast でも可

    std::cout << *cp << std::endl;
}

実行結果:

100

関数形式キャスト 🔗

もう1つ、実質的にキャストと呼べるものがあります。これは、関数呼び出しのように見える文法であることから、関数形式キャストと呼ばれます。

型名(式リスト)

式リストの部分は、関数呼び出し時に実引数を指定するのと同じで、式が1つならそのまま書き、2つ以上なら「,」で区切って書き入れます。空の場合もあります。

式が1つの場合は、その式の値が「型名」で指定した型にキャストされます。ここで行われるキャストは、C言語のキャストと同等です。

#include <iostream>

int main()
{
    std::cout << int(12.3) << std::endl;
}

実行結果:

12

式リストが空の場合はもはやキャストという感じではありませんが、値初期化(第7章)のルールに従って決められる値を持った型が作られます。

#include <iostream>

int main()
{
    std::cout << int() << std::endl;
}

実行結果:

0

また、これももはやキャストではありませんが、式リストに式を2つ以上指定する場合には、「型名」に指定する型はクラス型でなければなりません。指定した式は、コンストラクタに渡す実引数になります。

#include <iostream>

class MyClass {
public:
    MyClass(int x, int y) : mX(x), mY(y)
    {}

    void Print() const
    {
        std::cout << mX << ", " << mY << std::endl;
    }

private:
    int mX;
    int mY;
};

int main()
{
    MyClass(10, 20).Print();
}

実行結果:

10, 20


型変換演算子 🔗

クラスに、型変換演算子を定義しておくと、これを定義したクラスのオブジェクトを、別の型へ変換できます。

型変換演算子は、メンバ関数であると考えれば良いです。引数は無く、戻り値で変換後の値を返します。

型変換演算子は、名前の付け方が特殊で、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*)


解答ページはこちら

参考リンク 🔗


更新履歴 🔗

 C++編の同内容を扱っている箇所の記述に合わせるように修正。
関数形式キャスト」の項を追加。

 explicit を指定子と表記するように修正。

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

 新規作成。



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

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

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

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る