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

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

先頭へ戻る

この章と同じ(または似た)情報を扱うページが、Modern C++編 (C++11/14/17 対応) の以下の章にあります。

この章の概要

この章の概要です。


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

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_cast<キャスト後の型>(式);

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

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

ポインタや参照に付いている const や volatile を外すことができます。

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

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

#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


C++11 (列挙型の基盤となる型の指定)

C++11

C++11 では、列挙型の基盤型を指定できるようになっています。基盤型とは、列挙定数の型のことです。

enum タグ名 : 基盤型 {
    列挙子,
    列挙子,
      :
};

基盤型として指定できるのは、整数型だけです。

以下は具体例です。ここでは、基盤型を short型にしています。

#include <iostream>

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

    std::cout << std::boolalpha
              << (sizeof(Color) == sizeof(int)) << "\n"
              << (sizeof(Color) == sizeof(short)) << std::endl;
}

実行結果:

false
true

基盤型を指定しなかった場合、すべての列挙定数の値を表現できる大きさを持った整数型がコンパイラによって選択されます。ただし、sizeof(int) よりも大きい型が選ばれることはありません。

C++11 (スコープ付きの列挙型)

C++11

通常の列挙型では、列挙定数が他の名前と衝突してしまいます。C++11 では、この問題を避けるために、スコープ付きの列挙型が定義できるようになっています。また、従来の列挙型よりも型変換の厳しさが増しており、安全性が高まっています。

スコープ付き列挙型は、「enum class」あるいは「enum struct」のように、2つのキーワードを並べて定義します。両者の意味は同じです。

enum class タグ名 {
    列挙子,
    列挙子,
      :
};
enum struct タグ名 {
    列挙子,
    列挙子,
      :
};

基盤型(前の項)を指定することもできます。

enum class タグ名 : 基盤型 {
    列挙子,
    列挙子,
      :
};
enum struct タグ名 : 基盤型 {
    列挙子,
    列挙子,
      :
};

従来の列挙型と違って、基盤型を指定しなかった場合の型は int型であると定められています。

以下は具体例です。

#include <iostream>

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

    Color color = Color::GREEN;
    short s = static_cast<short>(color);  // キャストが必要

    std::cout << s << std::endl;
}

実行結果:

1

列挙定数にアクセスする際には、「列挙型の名前::列挙定数の名前」のように、スコープ解決演算子を使用します。

従来の列挙型とは違って、スコープ付き列挙型から整数型へは暗黙的に変換できません。必要であれば、明示的にキャスト(static_cast が選択できます)しなければなりません。

C++11 (auto)

C++11

C言語には、auto という、今となっては時代遅れのキーワードが存在しています(C言語編第22章参照)。C言語および、C++03以前の C++ では意味のないキーワードでしたが、C++11 ではまったく異なる意味・用途に変更されました。

C++11 での autoキーワードは、型推論を意味します。

型名を記述すべき箇所で「auto」と記述すると、コンパイラが、適切な型を文脈から判断してくれるというものです。例えば、次のように使うことができます。

#include <iostream>

int main()
{
    auto num = 100;

    std::cout << num << std::endl;
}

実行結果:

100

変数num には初期化子として 100 が与えられています。これは int型の値であると判断できます。結果、次のように書いたかのようにコンパイルが行われます。

int num = 100;

コンパイラが型を判断できるだけの情報があれば良いので、例えば、次のような使い方もできます。

#include <iostream>

double func();

int main()
{
    auto num = func();

    std::cout << num << std::endl;
}

double func()
{
    return 3.5;
}

実行結果:

3.5

変数num の初期値は、func() の戻り値によって決まります。func() の宣言を見れば、戻り値が double型であることが分かるので、変数num の型は double であると推論されます。

C++11 (decltype)

C++11

C++11 で追加された decltype を使うと、式から型を得ることができます。

#include <iostream>

double func();

int main()
{
    double a = 10.0;
    decltype(a) b = 20.0;
    decltype(func()) c = 30.0;

    std::cout << a << "\n"
              << b << "\n"
              << c << std::endl;
}

double func()
{
    std::cout << "func()" << std::endl;
    return 0;
}

実行結果:

10
20
30

decltype の直後の ( ) 内に記述した式の結果の型に置き換わります。

decltype(func()) のような記述で、戻り値の型も得られます。この場合、func関数が呼び出されている訳ではありません。実際、実行結果に "func()" が出力されていないことが分かると思います。


練習問題

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


解答ページはこちら

参考リンク



更新履歴

'2018/8/14 全体的に見直し修正。
-- 「関数形式キャスト」の項を追加。
-- 第2章から「C++11 (autoキーワード)」の項の内容を、「C++11 (auto)」へ移動してきた。
-- 「ヌルポインタの表現」の項を第2章へ移動。

'2018/7/29 「C++11 (nullptr)」の項の内容を削除。同じ内容を解説している Modern C++編のページへのリンクだけを残した。

'2018/4/2 「VisualC++」という表現を「VisualStudio」に統一。

'2018/2/22 「サイズ」という表記について表現を統一。 型のサイズ(バイト数)を表しているところは「大きさ」、要素数を表しているところは「要素数」。

'2018/1/5 コンパイラの対応状況について、対応している場合は明記しない方針にした。
Xcode 8.3.3 を clang 5.0.0 に置き換え。

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



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

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

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

Programming Place Plus のトップページへ


はてなブックマーク Pocket に保存 Twitter でツイート Twitter をフォロー
Facebook でシェア Google+ で共有 LINE で送る rss1.0 取得ボタン RSS