C++ の型とキャスト | Programming Place Plus C++編【言語解説】 第7章

トップページC++編

C++編で扱っている C++ は 2003年に登場した C++03 という、とても古いバージョンのものです。C++ はその後、C++11 -> C++14 -> C++17 -> C++20 -> C++23 と更新されています。
なかでも C++11 での更新は非常に大きなものであり、これから C++ の学習を始めるのなら、C++11 よりも古いバージョンを対象にするべきではありません。特に事情がないなら、新しい C++ を学んでください。 当サイトでは、C++14 をベースにした新C++編を作成中です。

この章の概要

この章の概要です。

関連する話題が、以下のページにあります。

C++ のキャスト

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

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

dynamic_cast は、これまでに解説していない機能と密接に関わるものなので、第31章で解説します。

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 を取り除くようなキャストは拒否されます

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

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 のそのほかの用途に、派生クラスへのダウンキャストがあります(第31章参照)。

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 を外すことはできません。

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

#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 ではメモリアドレスの調整が起こらないので、正しくキャストできません(第31章参照)。

const_cast

const_cast は、const修飾子や volatile修飾子に関するキャストで、ポインタや参照に付いている const や volatile を外せます。

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

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

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

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

#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 を付加する必要性があります(第15章参照)。

#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

式リストが空の場合はもはやキャストという感じではありませんが、値初期化というルールに従って決められる値を持った型が作られます。ここまでの章の知識だけでは理解しきれませんし、そもそも複雑なルールなので覚える必要もありませんが、以下にまとめておきます

【上級】式リストに式を2つ以上指定するには、「型名」に指定する型が、クラス型(第11章)でなければなりません。指定した式は、コンストラクタに渡す実引数になります。

値初期化

値初期化は、以下のように初期化を行います。初期化する対象の型のことを T と表現します。

  1. T が、明示的に定義したコンストラクタ(第13章)を持っている場合、デフォルトコンストラクタ(第13章)を呼ぶ。ただし、デフォルトコンストラクタにアクセスできなければコンパイルエラーになる
  2. T が、明示的に定義したコンストラクタを持っていない、union 以外のクラス型の場合、すべての 非staticなメンバ変数と、基底クラス(第26章)の構成要素に対して、値初期化を行う
  3. T が、配列型の場合は、それぞれの要素に対して、値初期化を行う
  4. その他の場合は、ゼロ初期化を行う

ゼロ初期化という言葉も登場しますが、これも初期化のルールの1つです。これはこの後取り上げます

ゼロ初期化

ゼロ初期化は、以下のように初期化を行います。初期化する対象の型のことを T と表現します。

  1. T がスカラ型(後述)の場合、0 を T型に変換して与える
  2. T が union 以外のクラス型の場合、非staticなメンバ変数と、基底クラスの非staticなメンバ変数はそれぞれゼロ初期化される
  3. T が union の場合、最初の非staticなメンバが、ゼロ初期化される
  4. T が配列型の場合は、それぞれの要素に対して、ゼロ初期化を行う
  5. T が参照型(第16章)の場合は、初期化を行わない

スカラ型は、整数型、浮動小数点数型、enum型、ポインタ型のことです。const や volatile のような修飾子が付いていても構いません。

bool型

bool型は、論理値を扱う型です。真を表すリテラルは true、偽を表すリテラルは false です。この2つ以外の値を持つことはできません。

bool b1 = true;
bool b2 = false;

bool型は分類上、整数型の一種です。そのため、汎整数拡張(C言語編第21章)の対象に含まれており、int型へ変換されます。false は 0 に、true は 1 に変換されます。

#include <iostream>

int main()
{
    int n = 100;

    n = false;
    std::cout << n << std::endl;

    n = true;
    std::cout << n << std::endl;
}

実行結果:

0
1

整数型、浮動小数点型、列挙型、ポインタ型の値は、bool型へ暗黙的に変換できます。元の値が 0 やヌルポインタであるときは false に変換され、それ以外のときは true に変換されます。

#include <iostream>

int main()
{
    enum E { e1, e2 };

    bool b1 = 0;
    bool b2 = 1;
    bool b3 = 0.0;
    bool b4 = 0.1;
    bool b5 = e1;
    bool b6 = e2;
    bool b7 = &b1;
    bool b8 = &b2;
    bool b9 = NULL;

    std::cout << b1 << "\n"
              << b2 << "\n"
              << b3 << "\n"
              << b4 << "\n"
              << b5 << "\n"
              << b6 << "\n"
              << b7 << "\n"
              << b8 << "\n"
              << b9 << std::endl;
}

実行結果:

0
1
0
1
0
1
1
1
0

実行結果は 0 か 1 で出力されてしまっていますが、必要があれば、std::boolalphaマニピュレータを使えば、true や false という文字列で出力できます。

#include <iostream>

int main()
{
    enum E { e1, e2 };

    bool b1 = 0;
    bool b2 = 1;
    bool b3 = 0.0;
    bool b4 = 0.1;
    bool b5 = e1;
    bool b6 = e2;
    bool b7 = &b1;
    bool b8 = &b2;
    bool b9 = NULL;

    std::cout << std::boolalpha
              << b1 << "\n"
              << b2 << "\n"
              << b3 << "\n"
              << b4 << "\n"
              << b5 << "\n"
              << b6 << "\n"
              << b7 << "\n"
              << b8 << "\n"
              << b9 << std::endl;
}

実行結果:

false
true
false
true
false
true
true
true
false

std::noboolalphaマニピュレータを使うと元の挙動、つまり論理値を整数値で出力する動作に戻ります。

!演算子は、オペランドを bool型に変換し、それを否定して返す演算子であるといえます。

#include <iostream>

int main()
{
    int n = 0;

    bool b1 = false;
    bool b2 = !b1;
    bool b3 = !n;

    std::cout << std::boolalpha
              << b1 << "\n"
              << b2 << "\n"
              << b3 << std::endl;
}

実行結果:

false
true
true


練習問題

問題① 次の各変換は、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*)


解答ページはこちら

参考リンク


更新履歴

≪さらに古い更新履歴を展開する≫



前の章へ (第6章 ファイルストリームの基礎)

次の章へ (第8章 関数オーバーロード)

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

Programming Place Plus のトップページへ



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