組込み現場の「C++」プログラミング 明日から使える徹底入門

高木 信尚(株式会社クローバーフィールド

1.3 「ベターC」としてのC++はこんなに便利

「ベターC」(Better C)というのは,文字どおり「より良いC」のことです.何をもって「より良い」とするかは人にもよりますが,ここでは,Cよりもエラーチェックが強力であり,いくつかの便利な拡張機能を使える点に注目して,ベターCとしてのC++について解説することにします.

1.3.1 Cとまったく同じコードをC++で書く

C++を使うのであれば,C++らしいコード,たとえば,クラスを積極的に使うべきだと主張する人も少なくありません.しかし,熟練したCプログラマーの場合,それまでの手法をいきなり捨てて,新しいことに手を出すより,もっと手軽にC++の良いところ取りをしてもよいのではないでしょうか?

C++を使うと,Cとまったく同じコードを書いた場合でも,潜在的なミスを検出することができるようになります.その多くは,Cの場合でもコンパイラが警告を出すものかもしれません.しかし,警告が出るかどうかはコンパイラに依存しますし,警告が出てもコンパイルはできてしまうので,それらのミスが見逃されることも少なくありません.

たとえば,次のようなコードを考えてみましょう.

const char str[] = "abc";
void func(char* s)
{
    s[0] = 'A';
}
int main()
{
    func(str); // ← 間違い! 
    return 0;
}

このコードをCとしてコンパイルすると,警告が出ることはあってもエラーになることはありません.コンパイルに成功し,実行もできてしまうことでしょう.その結果,ROMに配置された配列strに書き込みを行ったことで,CPU例外が発生したり,単に書き込みが無視されるなど,実行してみて初めて,おかしな動作に気づくことになるでしょう.へたをすると,気づかないことさえありえます.しかし,C++としてコンパイルすれば,「const char*からchar*への暗黙的な型変換はできない」といった内容のエラーが確実に発生します.警告ではなくエラーですので,コンパイルに成功することも,実行することもできません.コンパイラに駄目出しされたプログラマーは,否が応でも原因を調べて,対策を行わなければならなくなるのです.

別の例を見てみましょう.

int hex_to_int(int hex)
{
    int result = -1;
    if ('0' <= hex && hex <= '9')
    {
        result = hex - '0';
    }
    else if ('A' <= hex && hex <= 'F')
    {
        result = hex - 'A' + 10;
    }
    return result;
}
int main()
{
    int value = hex_to_int("B");
    return 0;
}

このコードでは,16進数字を受け取って整数に変換するhex_to_int関数を定義しています.ここで,hex_to_intが小文字に対応していないとか,'A'から'F'の連続性が規格上保証されないといったことは議論の本質ではありません.hex_to_int関数は16進数字を文字,すなわち整数値として受け取るように設計されています.ところが,main関数では,hex_to_int関数に"B"という文字列を渡してしまっています.これは明らかに間違っていますが,幸か不幸か,Cではポインタ型から整数型への暗黙的な型変換が可能です.その結果,Cコンパイラは,警告を出すことはあるかもしれませんが,このコードをエラーにすることはありません.しかし,C++コンパイラは,このコードを確実にエラーにしてくれます.

もう1つ,例を見てみましょう.今度はソースコードが2つになります.

【main.c】

// main.c
int main()
{
    int value = sub(123);
    return 0;
}

【sub.c】

// sub.c
int sub(long arg)
{
     …
    return 0;
}

main.cで定義されたmain関数は,sub.cで定義されたsub関数を呼び出しています.多くの場合,Cコンパイラはこのコードに対して警告すら出しません.そして,ターゲットが32ビットCPUであれば,多くの場合,何の問題もなく動作することでしょう.

int型が32ビットの環境から16ビットの環境に移植する機会はそう多くないかもしれませんが,数年後,long型が64ビットの環境に移植しようとしたとき,このコードは突然暴れ始めます.main関数の中でsub関数を呼び出すとき,実引数をint型(123はint型です)で渡していますが,実際にsub関数が要求しているのはlong型です.呼び出し元と呼び出し先の間に矛盾がありますから,このプログラムは,もはやどんな動きをするかわかったものではありません.

この問題の原因の1つは,Cでは,関数原型がなくてもコンパイルできてしまう点にあります.関数原型という仕様は,もともとC++で導入され,それがCの標準化の際に逆輸入されたものです.標準Cでは,過去の仕様との互換性を維持するために,関数原型がなくてもコンパイルできてしまうのです.

では,main.cの最初にでも,次の関数原型を入れてみましょう.

int sub(int arg);

強制はされていませんが,Cでも関数原型を使うことは可能ですから.ところが,すでにお気づきの方もいるでしょうが,この関数原型は間違っています.引数はint型ではなく,long型でなければならないのです.Cでは,関数原型をsub.hというヘッダファイルの中に記述し,それをmain.cとsub.cの両方からインクルードすることで間違いを検出することができます.けれども,もし,sub.cからsub.hをインクルードするのを忘れた場合には,どうすることもできません.しかし,C++を使えば,仮にsub.cからsub.hをインクルードするのを忘れたとしても,リンカが確実にエラーにしてくれます.C++の関数は,リンク時にエラーを検出できるように,引数の型情報をリンクのためのシンボル情報に埋め込んでいるのです.

このように,C++は,Cに比べると静的な型安全性がかなり強化されています.Cでプログラミングするとき,行儀の良いコーディングをされていた方であれば,C++を使ったとしても,おそらく何の違和感もないと思います.しかし,Cの曖昧さに依存した,たまたま動くコードを書いていた方にとっては,C++を使ったとたん,「なかなかコンパイルが通ってくれない」と,不平の1つもいいたくなることでしょう.しかし,間違ったコードをなんとか許してくれるコンパイラより,間違いは間違いと叱ってくれるコンパイラのほうが,より良いことは確かでしょう.

COLUMN C++の静的な型安全性も完璧ではない

C++の歴史は決して短くありません.その間,C++の言語仕様は何度も改定が行われてきました.その結果,Cほどではないにせよ,C++も過去のしがらみを背負っています.その中には,静的な型安全性をほころびさせるようなものもあります.1.3.1に出てきた次のコードを対象にして考えてみましょう.

void func(char* s)
{
    s[0] = 'A';
}

これに対し,次のようにした場合はエラーになります.

const char str[] = "abc";
func(str);

しかし,次のようにした場合にはエラーになりません."abc"という文字列リテラルの型はconst char[4]であるにもかかわらずです.

func("abc");

これは,文字列リテラルを使って,直接char*型のポインタに代入したり,初期化したりするコードが,過去にかなりの量存在していたために,やむなくchar*型の右辺値に暗黙的に型変換できる仕様になってしまったためです.暗黙的に型変換できるとしても,正しい型はあくまでもconst charの配列型であり,原則的にはROMに配置されることになります.当然,文字列リテラルの各要素に書き込むことはできません.

このように,C++の静的な型安全性も,必ずしも完璧ではないのです.完璧ではないとはいえ,ほとんど無きに等しいCの静的な型安全性に比べればはるかに優れています.こうした注意点を把握しておくことで,C++のメリットを十分に享受することができるのです.

1.3.2 些細な拡張と相違点

以下に,Cから拡張された点および相違点について説明します.

行コメント

C++では,Cと同じ/*で始まり*/で終わるブロックコメント以外に,//で始まり改行で終わる行コメントを使うことができます.行コメントは,多くのCコンパイラでも独自拡張機能として使用できますし,C99からはCの標準規格にも取り込まれました.

インライン関数

最近では,Cでも,多くの処理系が独自拡張としてインライン関数をサポートしています.また,C99ではインライン関数が標準規格に取り込まれました.しかし,Cのインライン関数には方言が多く,C99で規格化されたインライン関数であっても,大きな制約があります.たとえば,外部結合を持つインライン関数は,関数の中で静的オブジェクトを宣言してはならないとか,内部結合を持つ静的オブジェクトを参照してはならないといった制約です.

一方,C++はインライン関数は最初からサポートしていました.Cの多くの処理系では,C99だからではなく,独自拡張としてインライン関数をサポートしているのに対して,C++では標準の仕様ですので,インライン関数はずっと使いやすいものになっています.標準準拠度の低い処理系の場合には若干の方言もあるようですが,それでもCのインライン関数に比べればかなりましです.

タグ名だけで型名を表す

Cでは,構造体や列挙体などの型名は,「struct タグ名」のように,structやenumなどのキーワードをタグ名の前に付けなければなりませんでした.しかし,これは面倒なので,次のようにtypedef名を定義することが多いかと思います.

typedef struct foo_tag foo;

しかし,C++では,structなどを付けなくても,タグ名だけで型名とすることができます.したがって,次のコードは,単にfooと記述するだけでかまいません.

struct foo;

もちろん,Cと同じようにstruct fooと記述してもかまいませんが,Cとの互換性以上の用途はないでしょう.

なお,Cの場合には,structなどのキーワードを明示的に指定しなければならない関係上,タグ名と同じ名前の関数などを作ることができました.

たとえば,次のようにです.

struct foo;
int foo(int arg);

しかし,C++では,struct fooとfooは同じ意味になりましたので,上記のコードはコンパイルすることができません.

仮引数並びのない関数

Cの関数宣言を行う場合,仮引数並びを省略すると,それは引数の型チェックをまったく行わないことを意味します.すなわち,どんな型の実引数を何個渡したとしても,コンパイラはいっさいエラーにはしないのです.この仕様は正しく理解されていないことも多いようですので,ここで少し復習しておきましょう.

【Cの場合】

// Cの場合
void func(); // ← 仮引数並びがない関数 
struct A
{
     …
};
int main()
{
    A x;
    func();           // ← OK!
    func(123);        // ← OK! 
    func(1.23, 4.56); // ← OK! 
    func(x);          // ← OK! 
    return 0;
}

Cでは,関数が引数を持たないことを示すには,明示的にvoidを書かなければなりませんでした.

しかし,標準化以前の古いCならいざしらず,関数原型を書くのが当然になった今日では,仮引数リストを省略すれば,それは引数を持たないと考えるほうが自然です.C++では,Cとの互換性をあえて捨て,仮引数リストがない場合はvoidを指定したのと同じ意味になります.もちろん,C++でも,明示的にvoidを記述してかまいません.

【C++の場合】

// C++の場合
void func(); // ← 仮引数リストがない関数 
struct A
{
     …
};
int main()
{
    A x;
    func();           // ← OK! 
    func(123);        // ← エラー! 
    func(1.23, 4.56); // ← エラー! 
    func(x);          // ← エラー! 
    return 0;
}

ときには,C++でも実引数の型チェックを無視したいこともあります.その場合は,明示的に「...」を指定することで,引数の型チェックが行われないようにすることができます.こんなふうにです.

void func(...);

この仕様には,CとC++の間の互換性がありませんので,CとC++に共通のヘッダファイルを記述する場合は注意が必要です.

省略時実引数

関数の汎用性を上げようとすると,どうしても引数の数が増えてしまいます.あまり引数が多いのはみっともないので.ある程度構造体にまとめたりするかと思いますが,そうすると今度は,構造体を設定する必要が出てくることもあって,使い勝手が悪くなります.たくさんある引数のうちの多くは,だいたいいつも同じ値を指定すればよく,ときどき別の値にするというのが,よくあるパターンではないでしょうか?

そんな場合には,引数にデフォルトの値を設定し,デフォルト以外の値を指定する場合以外は省略できると便利です.C++の「省略時実引数」は,まさにそのための機能です.

void f(int a = 1, long b = 2, double c = 3.0);
int main()
{
    f();          // ← f(1, 2, 3.0);
    f(0);         // ← f(0, 2, 3.0);
    f(0, 5);      // ← f(0, 5, 3.0);
    f(0, 5, 7.0); // ← f(0, 5, 7.0); 
    return 0;
}

上記のサンプルコードのように,仮引数の直後に「= 値」として省略時実引数を設定することで,その仮引数に対応する実引数を省略したときには,設定した値が自動的に渡されます.省略時実引数は必ず後ろ詰めで設定しなければなりません.具体的には,次のようになるということです.

void func(int a, int b, int c = 3);         // ← OK! 
void func(int a, int b = 2, int c = 3);     // ← OK! 
void func(int a = 1, int b = 2, int c = 3); // ← OK! 
void func(int a = 1, int b, int c);         // ← エラー!
void func(int a = 1, int b, int c = 3);     // ← エラー! 
void func(int a, int b = 2, int c);         // ← エラー! 

省略時実引数は,単に記述を簡略化するだけのものです.記述を省略したとしても,実引数は関数に普通に渡されるので,効率が上がることもなければ,低下することもありません.

void*型と他のポインタ型との間の型変換

Cでは,void*型と他の型へのポインタ型との間は,相互に暗黙的な型変換を行うことができます.しかし,C++では,より安全なほうに倒すという観点から,関数型以外へのポインタ型からvoid*型には暗黙的に型変換することができますが,void*型から他の型へのポインタ型へは,明示的にキャストしないかぎり,型変換することができません.

int* p_int;
void* p_void;
void (*p_func)();
p_void = p_int;  // ← OK! 
p_int  = p_void; // ← エラー! 
p_void = p_func; // ← エラー! 
p_func = p_void; // ← エラー! 

Cでは,関数型へのポインタ型をvoid*型のオブジェクト(変数)に格納することもできましたが,それは,単にCの型チェックが甘いために可能であったことであり,本来は行うべきではありません.なぜなら,データとプログラムコードでは,メモリ空間が異なるアーキテクチャや,near領域とfar領域のように,ポインタのサイズが異なる環境があるからです.そのため,C++では,強引なキャストを行わないかぎり,関数型へのポインタ型からvoid*へ変換することはできません.

void*は静的な型チェックを無効にしてしまうため,C++では,可能なかぎり使うべきではありません.

wchar_t型とbool型

Cにもwchar_t型はありますが,それは単なるtypedef名でしかありません.C++では,wchar_t型は基本データ型の1つであり,wchar_tはキーワードです.

C++には,真偽値を表すためのbool型もあります.bool型のリテラルとして,trueとfalseがあり,それぞれ1と0の値に評価されます.bool型は通常1バイトですが,規格上は,bool型のサイズは処理系定義になります.Cの場合,等価演算子や関係演算子,!演算子の評価結果はint型ですが,C++の場合はbool型になります.

C99にもbool型がありますが,それは_Bool型に展開されるマクロにすぎません.また,C99のtrueやfalseは,それぞれ1と0に展開されるマクロです(すなわちint型になります).C99の_Bool型とC++のbool型の間に互換性があるかどうかはなんともいえないところですので,C99とC++の両方に共通するコードでは,_Bool型と(C++の)bool型の使用は避けたほうが無難です.

ブロックの先頭以外での宣言

Cでは,原則として,ブロックの先頭でしか宣言を行うことができません.しかし,これでは,宣言した場所と,実際に使用する場所が遠く離れてしまいます.また,第2章で詳しく解説しますが,クラスを使用する場合には,オブジェクト(変数)の宣言を行った時点で,必ずしも軽量とはいえない初期化処理が実行される場合があります.これをすべてブロックの先頭で行う場合,条件次第ではそのオブジェクトはいっさい使用されないにもかかわらず,初期化のためのコストがかかってしまいます.

こうした問題を防ぐため,C++では,宣言も一種の文として扱われます.すなわち,文を記述できるところであれば,どこでも宣言を行うことができるのです.

int main()
{
    f();
    char buf[16]; // ← ブロックの途中で宣言 
    g(buf);
    return 0;
}

上記のサンプルコードと同じことをCで行うには,次のようにしなければなりませんでした.

int main()
{
    f();
    {
        char buf[16]; // ← これでもブロックの先頭なので宣言可 
        g(buf);
    }
    return 0;
}

逆にいえば,このようにそのつどブロックを作れば,Cであっても,どこでも宣言は書けるわけです.その意味で,ブロックの途中で宣言ができるという仕様は,単なる記述の簡略化ということだけであるともいえます.なお,Cであっても,C99からはブロックの途中で宣言ができるようになりました.

C++の宣言は文なので,文が記述できるところであればどこでも宣言できると書きました.しかし,実はもう少し宣言できる場所があるのです.それは,このような場所です.

int main()
{
    for (int i = 0; i < 10; i++) // ← for初期化文で宣言 
    {
        printf("%d\n", i);
    }
    return 0;
}

Cでは,forの最初の節には式しか書けませんでした.しかし,C++では,forの第1節は「for初期化文」という一種の文の扱いになります.ただし,for初期化文として記述できるのは,式文と宣言文だけです.複合文やラベル付き文やgoto文などを記述することはできません.なお,forの第1節で宣言ができるという仕様は,C99にも導入されました. C++では,さらにもう1つ宣言を記述できる文脈があります.それは,制御文の条件です.

次のように,制御文の条件として宣言を行うことができます.

int main()
{
    const char str[] = "abc";
    for (int i; char ch = str[i]; i++)
    {
         …
    }
    const char* s = str;
    while (char ch = *s++)
    {
         …
    }
    if (int value = f())
    {
         …
    }
    switch (int value = g())
    {
    case 1:
         …
    case 2:
         …
    }
    return 0;
}

ただし,条件として宣言することができるのはbool型に評価可能な型に限られます.そして,宣言したオブジェクトがtrueであれば真,falseであれば偽として扱われます.

なお,この仕様はC99にも導入されておらず,いまのところはC++固有のものです.

1.3.3 「const修飾子」はかなり異なる

const修飾子はCにもありましたが,C++のconst修飾子はCのconst修飾子とはやや異なります.まず,関数の外で宣言されたconst修飾付きのオブジェクト(変数)は,デフォルトでは内部結合になります.

const int foo = 123;

すなわち,上記のコードは,次と同じ意味になるのです.

static const int foo = 123;

Cでは,const修飾子があろうがなかろうが,デフォルトでは外部結合ですから,これもまた,CとC++の非互換性の1つです.const修飾されたオブジェクトを外部結合にするには,明示的にextern指定子を付ける必要があります.ただし,次のような場合は外部結合になります.

const char *str = "abc";

このconst修飾子はstrそのものを修飾しているのではなく,ポインタの参照先の型を修飾しているからです.もちろん,次の場合には内部結合になります.

const char* const str = "abc";

const修飾子が付いた場合の相違点はもう1つあります.const修飾され,かつvolatile修飾されていない汎整数型または列挙型のオブジェクトは,汎整数定数式の中で使ってもよいことになっています.こう書くと難しいですが,要するに,次に示す内容を意味しています.

const int x = 10; // ← const修飾された汎整数型のオブジェクトx
int array[x]; // ← 配列の要素数は汎整数定数式でなければならない 
enum
{
    y = x // ← 列挙子の値指定は汎整数定数式でなければならない 
};
struct A
{
    int a : x; // ← ビットフィールドは汎整数定数式でなければならない 
};
void func(int arg)
{
    switch (arg)
    {
         …
    case x: // ← caseラベルの値は汎整数定数式でなければならない 
         …
    }
}

ところで,Cの場合には,上記のサンプルコードで示した場合に加えて,定数式でなければならない状況として,初期化子がありました.しかし,C++の場合,静的記憶域期間を持つオブジェクトの場合を含めて,初期化子は定数式でなくてもかまいません.

const int array[] = { func() };
int main()
{
     …
}

たとえば,上記のように,初期化子の中で関数を呼び出してもかまわないのです.それも関数の外で定義するオブジェクトで.

これは組込み開発の場合には非常に重要な意味を持ちます.すなわち,const修飾された静的オブジェクトであっても,必ずしもROMに配置されるとはかぎらないのです.確実にオブジェクトをROMに配置したい場合には,定数式以外を初期化子に使わないよう,十分注意する必要があります.これを怠ると,予想に反してRAMを大量に消費してしまいます.

このように,定数式以外で行われる初期化のことを「動的初期化」といいます.動的初期化は,原則として,main関数(またはそれに代わる関数)が呼び出される前に行われます.当然,スタートアップルーチンでは,そのための処理を記述する必要があります.動的初期化のための記述をどのように行うかは,処理系ごとに異なります.

1.3.4 ポインタではない,文字どおりの「参照型」

Cでは,特定のオブジェクトを間接的に参照するには,ポインタを使うことになります.マクロを使って別名を付けるという方法もあるにはありますが,いろいろと弊害も多く,あまりお勧めできるものではありません.しかし,C++には,純粋にオブジェクトを参照するための「参照型」というものが存在します.参照型を使えば,マクロを使って別名を付けたときと同等の使い勝手と効率が得られ,しかも,マクロにまつわる弊害,たとえば,有効範囲(スコープ)が働かない,たまたま名前が同じであれば置換されてしまうといった問題も回避することができます.

参照型を使うには,次のように記述します.

struct A
{
     …
};
A a, b;
A& r = a; // ← rはaを参照する 
r = b;    // ← ここではbの値をaに代入する.参照先は後から変更できない
++r;      // ← aをインクリメント 

このサンプルコードが示すように,参照型は,宣言時にのみ参照先を設定することができます.その後は参照先を変更することができませんので,間違った操作で参照先が変わってしまうような心配はありません.また,宣言時には必ず参照先を指定しなければなりません.もし,参照先の指定を忘れるとコンパイルエラーになってしまいます.そのため,ポインタのように,うっかり未初期化のまま使ってしまうといった心配もないのです.

また,ポインタの場合は必ず型が一致しなければなりませんが,参照先の型にconst修飾を付けた参照型(以降,const参照と呼びます)の場合,参照先の型と一致していなくても,一時オブジェクトが生成されます.

const double& x = 123; // ← 一時オブジェクトが生成される.これは,下記と同じ意味になる 
// const double __temp = 123;
// const double& x = __temp;

この例では,一時オブジェクトが生成されるありがたみがわからないかもしれませんが,関数の引数にconst参照を使う場合には,非常に便利です.

void func(const long& arg);
int main()
{
    func(123);  // ← 一時オブジェクトが生成される(実引数はint型なので) 
    func(123L); // ← 本来なら,このように書かなければならない 
    return 0;
}

上記のサンプルコードでは,long型へのconst参照を渡すべきところに,int型の実引数を与えているために,いったんlong型の一時オブジェクトが生成され,その参照がfunc関数に渡されています.たとえ,int型とlong型のサイズが同じであったとしてもです.もし.一時オブジェクトが自動的に生成されなければ,そのつどキャストを行うなどして,本来の型に揃えなければならなくなります.

なお,自動的に一時オブジェクトが生成されるのは,const参照の場合だけです.非const参照であるにもかかわらず,勝手に一時オブジェクトが生成されてしまうと,関数の中でオブジェクトを更新したつもりでも,実は更新されたのは一時オブジェクトであって,更新されると期待していたオブジェクトは元のままといった不具合に繋がるからです.

1.3.5 名前の衝突を防ぐ「名前空間」

ある程度プログラムの規模が大きくなると,関数名をはじめとした名前の衝突が問題になってきます.Cでは,そうした問題を解消するために,モジュールごとに定めた接頭辞を付けることになります.しかし,これではどんどん名前が長くなり,なにかと不便です.C++には,この問題を解消するための「名前空間」という機能が備わっています.まずは,名前空間のイメージをつかんでいただくために,サンプルコードを示します.

namespace A
{
    int f(int arg); // ← A 
    void g()
    {
        f(1); // ← Aを呼び出す 
    }
}
namespace B
{
    int f(int arg); // ← B 
    void g()
    {
        f(2); // ← Bを呼び出す 
    }
}
int f(int arg); // ← C 
int main()
{
    A::f(1); // ← Aを呼び出す 
    B::f(2); // ← Bを呼び出す 
    f(3);    // ← Cを呼び出す 
    return 0;
}

このサンプルコードを見れば,何を意味しているのか,おおよその想像が付くのではないでしょうか.namespaceというキーワードは,名前空間を定義するために使用します.namespaceの直後に記述した名前(サンプルコードではAおよびB)が,それぞれの名前空間に付ける名前になります.そして,特定の名前空間を指定するには,A::のように,名前空間名のあとに2つのセミコロン(::)を付け,その後に,実際に使用したい関数などを指定します.

この2つのセミコロンは,「有効範囲解決演算子」といいます.文字どおり,有効範囲(スコープ)を解決するための演算子です.名前空間は,「namespace 名前空間名」に続く中括弧の中に1つの有効範囲を作り出すのです.Cでは,関数の外はファイル有効範囲でしたが,C++では,ファイル有効範囲というのはなく,関数の外は名前空間有効範囲になります.namespaceで囲まれていない一番外側も,大域的名前空間という一種の名前空間であると見なされます.なお,サンプルコードでは使用していませんが,明示的に大域的名前空間有効範囲を指定する場合には,「::」を単項演算子として使用します.具体的には,サンプルコードC)のf関数であれば,次のように記述することができます.

::f(3);

名前空間の外から名前空間の中の名前にアクセスするには,有効範囲解決演算子が必要ですが,名前空間の中で,同じ名前空間に属している名前にアクセスするのであれば,有効範囲解決演算子を用いる必要はありません.先ほどのサンプルコードの中でも,名前空間Aに属しているg関数からは,同じ名前空間に属しているf関数を,A::fとはせずに,単にfだけでアクセスしています.main関数の中で,大域的名前空間に属しているf関数にアクセスする際に::fとせずに単にfとしているのも,main関数もまた,大域的名前空間 に属しているからです.

名前空間の中で宣言できるのは,関数だけではありません.オブジェクト(変数)や型の定義を行うことももちろん可能です.ただし,マクロは名前空間が適用されません.そのため,C++ではマクロの使用は極力避け,代わりに列挙体やconstオブジェクトを使用するほうが適しています.

名前空間は入れ子にすることもできます.たとえば,次のように,何重にでも入れ子にすることができます.

namespace A
{
    namespace B
    {
        typedef int type;
    }
    B::type variable;
}
int main()
{
    A::B::type x = A::variable;
    return 0;
}

モジュールに専用の名前空間を与え,その中のサブモジュールごとに,入れ子にした個別の名前空間を与えることができるのです.入れ子にした名前空間に属している名前にアクセスするには,サンプルコードのA::B::typeのように,有効範囲解決演算子を使って,必要なだけ名前空間を並べることになります.

また,名前空間は一度にまとめて記述しなくても,同じ名前の名前空間は,同じ名前空間と見なされます.

namespace A
{
    int a;
}
void b();
namespace A
{
    int c(int);
}

たとえば,上記のように書いたとしても,最初に現れた名前空間Aと,最後に現れた名前空間Aは,同じものと見なされます.

有効範囲解決演算子によって名前空間を明示的に指定できることで,関数の中で同じ名前を宣言した場合でも,関数の外の名前にアクセスすることができます.すなわち,次のように,::fooとすることで,たとえ関数の中で同名のfooという名前を宣言していたとしても,関数の外のfooにアクセスすることができるのです.

int foo;
int main()
{
    int foo;
    foo = ::foo;
    return 0;
}

Cでは,有効範囲に応じた接頭辞を付けるようなコーディング規約もよく使われています.たとえば,グローバル変数にはg_を付けるなどです.しかし,C++ではg_のような接頭辞は不要で,大域的名前空間有効範囲にある名前であることを表すには,::を頭に付ければよいのです.

名前空間は記述上の便宜にすぎませんので,名前空間をどんなに使っても,サイズ的にも,実行時間的にも,まったくオーバーヘッドは発生しません.

名前空間の簡略化

名前空間は便利ですが,有効範囲解決演算子を用いてつねに名前空間を明示的に記述するのは面倒です.そこで,記述を簡略化するための方法が用意されています.

これも,まずはサンプルコードを示します.

namespace long_long_name_module
{
    int a;
    int b;
}
int main()
{    
    {
        namespace module = long_long_name_module; // ← 名前空間の別名 
        module::a = 0; // ← long_long_name_module::a = 0;と同じ意味 
        module::b = 1; // ← long_long_name_module::b = 1;と同じ意味 
    }
    {
        using long_long_name_module::a; // ← using宣言 
        a = 0; // ← long_long_name_module::a = 0;同じ意味 
        b = 1; // ← エラー! これはできない 
    }
    {
        using namespace long_long_name_module; // ← using指令 
        a = 0; // ← long_long_name_module::a = 0;と同じ意味 
        b = 1; // ← long_long_name_module::b = 1;と同じ意味 
    }
    return 0;
}

名前空間の記述を簡略化するための1つ目の方法は,別名の設定です.

次のようにすることで,名前空間に別名を付けることができます.

【名前空間に別名を付ける】

namespace 別名 = 名前空間名;

次のように,入れ子になった名前空間に別名を付けることもできます.

【入れ子になった名前空間に別名を付ける】

namespace A = B::C::D::E;

2つ目の方法は,using宣言です.using宣言は,usingというキーワードを用いて,特定の名前空間に属している特定の名前を指定することで,以降は名前空間を指定せずに名前にアクセスできるようになります.正確には,指定した名前空間に属している名前を,現在の名前空間に導入します.

次のようにすることで,以降は,「名前空間名::名前」の形式ではなく,単に「名前」とするだけでアクセスできるようになるのです.

【using宣言】

using 名前空間名::名前;

最後の方法は,using指令です.using宣言とまぎらわしいので注意してください.using宣言は,名前空間に属している特定の名前の記述だけを簡略化するためのものでしたが,using指令を使えば,指定した名前空間に属しているすべての名前の記述を簡略化することができます.正確には,指定した名前空間に属しているすべての名前を,現在の名前空間に導入します.

次のようにすることで,以降は,名前空間名を明示的に指定することなく,その名前空間に属している名前にアクセスできるようになります.

【using指令】

using namespace 名前空間名;

いずれの方法も,それを記述した有効範囲の中だけで通用します.

このように名前空間の記述を省略することで,ソースコードを簡潔に記述することができます.しかし,むやみに記述を省略すると,それぞれの名前の素性が不明確になりますし,有効範囲の名前なのかをコンパイラが判断できなくなれば,当然コンパイルエラーになります.可能なかぎり,名前空間の簡略化は割けたほうが無難です.どうしても簡略化を行う場合には,極力せまい有効範囲の中だけで行うようにし,なるべくusing指令よりはusing宣言を使うようにするべきです.

無名名前空間

ところで,名前空間にはもう1つ別の使い道があります.それは,他の翻訳単位*4から名前のアクセスができないようにするためのものです.これには,「無名名前空間」を使用します.無名名前空間というのは,下記のように,名前空間名を省略した名前空間のことです.

namespace
{
    void func()
    {
         …
    }
}

実際には,無名名前空間の名前空間名は,翻訳単位ごとにコンパイラが勝手に割り振ります.コンパイラがどんな名前空間名を割り振ったかを知るすべは一般的にはないので(コンパイル結果を見るなどすれば別ですが),他の翻訳単位からはアクセスできなくなります.なお,同じ翻訳単位からは,名前空間名を指定しなくても,普通にアクセスすることができます.上記のサンプルコードの場合,単にfuncまたは::funcとすれば,無名名前空間に属しているfuncにアクセスすることができます.

Cでは,こうした用途には,static指定子を付けることで内部結合にします.C++でも,普通の関数やオブジェクト(変数)の場合にはそれでもかまいません.しかし,第2章で詳しく解説しますが,クラスのメンバー関数の場合,この方法はうまくいきません.そのため,同じ翻訳単位の中でのみ使用する名前には,static指定子ではなく,つねに無名名前空間を使用することを推奨します.

実引数依存の名前検索

このように,名前の衝突を回避する手段として非常に便利な名前空間ですが,落とし穴がないわけでもありません.逆に,これから解説する点にさえ注意しておけば,名前空間は非常に強力な武器となります.これも,まずはサンプルコードを示します.

namespace A
{
    struct foo
    {
    };
    void bar(foo* arg);
}
int main()
{
    A::foo x;
    bar(&x);    // ← OK! 
    A::bar(&x); // ← OK! 
    ::bar(&x);  // ← これはエラー! 
    return 0;
}

上記のサンプルコードでは,名前空間Aに属しているbar関数を,A::を付けなくても呼び出すことができることを意味しています.これはどういうことかというと,bar関数の引数に,同じ名前空間に属しているfoo構造体へのポインタが含まれているからです.ポインタ渡しだけでなく,値渡しや参照渡しとしてfoo構造体を受け取る場合も同様です.この仕様は「実引数依存の名前検索」といいます.

実引数依存の名前検索は,それはそれで便利な仕様なのですが,注意しないと予期しない不具合を引き起こすことになります.特に,名前空間に後から関数を追加する場合は要注意です.具体的には,次のように,後からBのbar関数を追加するような場合です.

namespace A
{
    struct foo
    {
    };
}
void bar(A::foo* arg); // ← A 
namespace A
{
    void bar(foo* arg); // ← B ←後から追加 
}
int main()
{
    A::foo x;
    bar(&x); // ← エラー! 曖昧なため多重定義が解決できない
    return 0;
}

Bのbar関数を追加するまでは,main関数の中で呼び出しているbar関数はAのものでした.しかし,後からBを追加したことで,名前空間の外のコードまで破壊してしまったことになります.

開発の途中段階では,名前空間の中にどんどん新しい関数が追加されることと思います.しかし,ある程度詳細仕様が固まれば,それ以降は名前空間に新しい関数を追加すべきではありません.どうしても新しい関数を追加する場合は,名前空間の中に別の名前空間を入れ子にするなどして,実引数依存の名前検索に絡む問題を回避すべきです.

なお,標準準拠度の低い処理系の場合,実引数依存の名前検索が正しく実装されていない場合があります.名前空間を使う場合には,あらかじめコンパイラの挙動を調べておくことを強くお勧めします.たとえば,本来,実引数依存の名前検索によって名前空間を指定しなくてもよいはずのところで,コンパイルエラーが発生する場合があります.その場合は,つねに名前空間を指定することで回避してください.

*4 ごく大ざっぱにいえば,1つのソースファイルと,#include指令で取り込むすべてのヘッダやソースファイルを加えたもののことです.

1.3.6 同じ意味の関数には同じ名前を ―多重定義と関数テンプレート

Cでは,同じ名前の関数を複数定義することはできません.意味は同じでも引数が異なれば,それぞれに別の名前を付ける必要があります.最も簡単な例として,絶対値を求める標準関数を取り上げてみましょう.

int abs(int arg);
long labs(long arg);
double fabs(double arg);

上のコードは,すべて数値の絶対値を求める関数であり,意味は同じです.しかし,扱う型だけが異なるのです.このような関数は,できればすべてabsという名前で扱いたいものです.

C++には,多重定義または関数テンプレートという機能があります.そのどちらかを用いることで,引数の型や個数が異なる関数に同じ名前を付けるこができます.

多重定義

多重定義を使うのは簡単です.引数の型や個数が異なる関数をそれぞれ宣言/定義するだけです.あとは,関数を呼び出すときに渡した実引数の型に応じて,適切な関数をコンパイラが選択してくれます.実引数の型は,関数の仮引数の型に完全に一致する必要はなく,暗黙的に変換することができるのであれば,変換後に一致する型の仮引数を持つ関数が選択されます.ただし,多重定義された関数のうち,どれを呼び出すべきかが曖昧な場合には,コンパイルエラーになります.

int abs(int arg);
long abs(long arg);
double abs(double arg);
int main()
{
    abs(-123);     // ← abs(int)が呼び出される 
    abs(-123L);    // ← abs(long)が呼び出される 
    abs(-123.0);   // ← abs(double)が呼び出される 
    abs('a');      // ← abs(int)が呼び出される 
    abs((short)1); // ← abs(int)が呼び出される 
    abs(123U);     // ← エラー! 曖昧 
    abs(123UL);    // ← エラー! 曖昧 
    return 0;
}

多重定義を使えば,サンプルコードのような数値を引数に取るabsだけでなく,複素数型やベクトル型のようなユーザー定義型の場合でも,まったく同じ構文で記述することができます.異なる型に対して同じ構文が適用できるということは,「1.1.3 ジェネリックプログラミング言語としてのC++」で解説したジェネリックプログラミングを行ううえでも非常に重要になります.

多重定義は実引数の型に応じて,呼び出すべき関数が選択されます.そのため,C++では,オブジェクトの型は値と同じぐらい重要です.たとえ,int型とlong型とが同じサイズだったとしても,型が異なる以上,多重定義を解決する際は,あくまでも別のものとして扱われます.特に,式の評価結果やリテラルの型が何になるのか,正確に理解していないと,デバッグのときなど,実際にどの関数が呼び出されるのかわからなくなります.先ほどのabs関数を使って考えてみましょう.

int main()
{
    short a = 1;
    abs(a);      // ← abs(int) 
    abs(a + 0L); // ← abs(long) 
    abs(65535);  // ← int型が16ビットならabs(long),そうでなければabs(int) 
    abs(0xffff); // ← int型が16ビットならエラー!,そうでなければabs(long) 
    return 0;
}

多重定義は,実引数の型によって,どの関数を呼び出すかを解決します.したがって,引数が同じで,返却値だけが異なるような多重定義を行うことはできません.また,引数の型が異なったとしても,実引数からだけでは判断できないような多重定義はコンパイルエラーになります.

たとえば,次のような場合です.

void func(int arg);
void func(const int& arg);
void func(int a, int b = 0);

関数テンプレート

多重定義を行えば,実引数の型に応じた同名の関数を定義することができます.しかし,多数の型について,それぞれ同じことを何度も書くのは手間ですし,保守性も悪くなります.また,未知のユーザー定義型まで含めた対応を行おうとすると,多重定義では無理があります.そんなときには「関数テンプレート」を使うことができます.先ほどのabs関数を関数テンプレートを使って定義すると,次のようになります.

template <typename T>
T abs(T arg)
{
    return arg < 0 ? -arg : arg;
}

関数テンプレートを定義しておけば,実際にそれが呼び出されるようなコードが記述されたときに初めて,関数の実体が生成されます.関数テンプレートは,実引数の型が異なれば,その数だけ実体が生成されます.たとえば,short型やsigned char型の場合には,int型版の関数が呼び出されればそれでよいのですが,それでも,short型版とsigned char型版が生成されます.abs関数のような小さな関数の場合はよいのですが,もっと大きな関数になると,プログラムサイズの肥大化にも繋がります.関数テンプレートによるプログラムサイズの肥大化を防ぐには,引数の型に関係ない共通部分を抜き出して,別の関数にするなどの工夫が不可欠です.

さて,上記の関数テンプレートの場合,数値型であればこれでよいのですが,複素数を表す構造体の場合には,関係演算子が使えないのでうまくいきません.そんな場合は,個別に多重定義を行ってもかまいませんが,特定の型に対して「テンプレートの特殊化」を行うこともできます.

struct complex
{
    double real;
    double imag;
};
template <>
complex abs(complex arg)
{
    return sqrt(arg.real * arg.real + arg.imag * arg.imag);
}

関数テンプレートの特殊化は,実際にはあまりメリットがない場合が多く,多くの場合,通常どおり,多重定義を行うほうが便利です.具体的には,本来であれば,次のように引数を参照渡しにしたほうがよい場合でも,テンプレートの特殊化の場合には,元のテンプレートと同じ形式でなければならない(すなわち,引数と返却値の型が一致しなければならない)ため,どうしても値渡しにしなければなりません.

complex abs(const complex& arg)
{
    return sqrt(arg.real * arg.real + arg.imag * arg.imag);
}

このように,関数テンプレートの特殊化は,融通が利かない場合が多いのです.

また,関数テンプレートは,次のような不便さも持っています.

次のような,2つの引数を比較して,大きいほうの値を返す関数テンプレートを考えてみましょう.

template <typename T>
T max(T a, T b)
{
    return a < b ? b : a;
}

max関数の第1引数にint型の値を,第2引数にlong型の値を渡そうとすると,テンプレートが解決できずにコンパイルエラーになります.なぜなら,max関数の2つの引数は同じ型でなければならないからです.

このような問題を解決するには,long型の引数を2つ持つ,テンプレートではないmax関数を多重定義しておくとよいでしょう.関数テンプレートが適用できないと知ったコンパイラは,多重定義されたmax関数を適用しようと試みます.多重定義された非テンプレートの関数の場合,引数の型はぴったり同じでなくても,暗黙的な型変換が可能であれば,それで呼び出すことができるからです.

このように,関数の多重定義と関数テンプレートは,どちらが優れているかとかではなく,相互補完的な関係にあるといえます.

ところで,多重定義は,引数が同じで返却値の型だけが異なる場合には使えませんでした.しかし,関数テンプレートは,引数が同じで返却値の型だけが異なる場合でも使うことができます.たとえば,次のようにです.

template <typename T>
T func(int arg)
{
     …
}
int main()
{
    int a = func<int>(123);
    double b = func<double>(123);
    return 0;
}

実引数から返却値の型を推測することはできませんので,返却値の型は明示的に<型>として指定する必要があります.また,次のように,返却値の型だけを明示的に指定し,引数の型は実引数から推測させることもできます.

template <typename T1, typename T2>
T1 func(T2 arg)
{
     …
}
int main()
{
    int a = func<int>(123.456);
    double b = func<double>(123UL);
    return 0;
}

さらには,次のように,引数の型まで明示的に指定することもできます.引数の型まで明示的に指定した場合には,実引数の暗黙的な型変換を期待することも可能になります.

func<int, double>(123.456);

COLUMN マクロの使用は控えよう

Cでは,さまざまな定数値や制御レジスタなどをマクロとして定義します.しかし,C++では,そうしたマクロの定義はできるかぎり行うべきではありません.

Cでも,マクロの問題はいろいろ指摘されています.関数形式マクロの場合には,危険な副作用を伴う場合がありますし,オブジェクト形式のマクロの場合でも,演算子の優先順位の関係で予期せぬ結果になったりするからです.

#define abs(arg)    ((arg) < 0 ? -(arg) : (arg))
y = abs(x++); // ← 危険な副作用 
#define X  x + 10
y = X * 2; // ← x + 10 * 2 → x + 20の意味に? 

さらに,C++の場合には,オブジェクト形式のマクロを評価した結果の型が不明確になるという問題があります.

#define M  (-32768)

このMの型が何になるか,直感的にわかるでしょうか? まず,int型が16ビットの場合について考えましょう.32767という値はint型の範囲を超えているのでlong型になります.long型に単項の-演算子を付けてもlong型です.次に,int型が16ビットより大きい(たとえば,32ビットの)場合を考えてみましょう.今度は,32768はint型の範囲に納まるのでint型です.int型に単項の-演算子を付けてもint型です.

このように,マクロの場合には,一見しただけでは型がわからない場合が多々あります.確かに,次のように,明示的にキャストしていればよいのかもしれませんが,このような記述を徹底することは困難なはずです.

#define M  ((int)(-32768))

C++では,関数を多重定義することができますから,型によって実際に呼び出される関数が変わります.ソースコードを見ただけで,どんな動作になるのかを正しく把握できるようにするためにも,オブジェクトの型は明確でなければなりません.

また,マクロの持つ本質的な問題として,有効範囲(スコープ)を無視して,あらゆる識別子を置換してしまう点が挙げられます.これでは,せっかくの名前空間も何の役にも立ちません.また,C++の場合には,インライン関数やテンプレート,それにクラス定義など,ヘッダファイルの中で記述する内容がCに比べてずっと多いのです.しかも,関数の中で使っている局所変数などは,実装の詳細ですから,インターフェース仕様にも現れてきません(しかも,実装者の気まぐれで頻繁に変更されるかもしれません).

厳格なコーディング規約を作ることで,この問題は回避できるかもしれません.しかし,マクロを使うことを避ければ,もっと確実で,楽に問題を解決できるのです.

では,マクロを使わずにどうすればよいかについても触れておきましょう.関数形式のマクロは,ほとんどの場合,インライン関数を使えば実現できます.インライン関数は,パフォーマンスを落とすことなく,危険な副作用の問題も回避できます.オブジェクト形式のマクロは,const修飾子付きの整数型オブジェクトまたは列挙型で置き換えることができます.浮動小数点数や文字列は,外部宣言に置き換えることができるはずです.

それでも,すべてのマクロをなくすことはできません.しかし,マクロを必要最小限に絞り込めば,問題が発生する確率はずっと低くなることでしょう.

1.3.7 例外処理

Cでは,呼び出した関数の中で何か異常が発生した場合,次のいずれかの方法を用いて異常を知らせ,処理することになります.

  • (A) 返却値としてエラーコードを返す.
  • (B) エラーコード格納用の変数へのポインタを引数として渡し,エラーコードを格納する.
  • (C) errnoのようなグローバル変数,またはそれに相当するものにエラーコードを格納する.
  • (D) シグナル処理ルーチンのようなコールバック関数を呼び出す.
  • (E) longjmp関数を用いて大域脱出する.

C++の「例外処理」は,このうちの(E)を構造化し,使い勝手を向上したものです.Cでは,setjmpマクロやlongjmp関数というのは,なじみがうすいうえに使い勝手も悪く,多くの危険を伴うために,使用を禁止している現場も多いと思います.しかし,C++の例外処理は,setjmpマクロとlongjmp関数の問題点の多くを解消し,非常に便利な機能を提供します. まずは,イメージがつかめるようにサンプルコードを示します.

#include <stdio.h>
struct E { …… };
int func(int arg)
{
    if (arg < 0)
    {
        E e;
        throw e;
    }
    return arg;
}
int main()
{
    try
    {
        int value = func(-1);
        printf("value = %d\n", value);
    }
    catch (E& e)
    {
        printf("例外発生!\n");
    }
    return 0;
}

例外処理を使うには,監視ブロック(try-Block)を記述します.try直後のブロックの中に,例外が発生するかもしれない処理を記述します.そして,例外が発生したときの処理(例外ハンドラ)をcatch以降のブロックの中に記述します.catchに続くカッコには,処理すべき例外オブジェクトの型を記述します.ちょうど,関数の仮引数のようなものだと考えてください.

例外を捕捉(catch)するときは必ず参照型を用います.値を捕捉することも言語仕様上は可能ですが,例外は参照型で受けるものだと覚えておいてください.

例外を発生させるには,func関数の中にあるように,throwというキーワードを使用します.throwの直後には,ちょうどreturn文のように,送出すべき例外オブジェクトを指定します.上記の例では,例外オブジェクトとしてE構造体のオブジェクトeを例外オブジェクトとして指定しています.throwを使ったこの記述を「送出式」といいます.文字どおり,これは式であって,returnのような文ではありません.送出式はvoid型の式としてコンパイラに評価されます.

処理が複雑になってくると,送出される例外オブジェクトも1種類ではなくなります.そうなると,例外オブジェクトの種類によって,例外ハンドラを使い分けたくなってくるはずです.そんな場合には,次のように,catchを複数記述することができます.

int main()
{
    try
    {
         …
    }
    catch (E1& e)
    {
         …
    }
    catch (E2& e)
    {
         …
    }
    catch (...)
    {
         …
    }
    return 0;
}

最後のcatchのように,カッコの中を「...」にすると,あらゆる型の例外オブジェクトをとらえることができます.catchを複数書いた場合,最初に書いたcatchから順に,例外オブジェクトの型が一致するかどうかを調べていきます.そして,一致するものが見つかれば,その例外ハンドラが実行され,以降のcatchは無視されます.もし,一致するものが1つもなければ,その関数の呼び出し元に例外処理を引き継ぐことになります.どこまでいっても一致するcatchが見つからなければ,プログラムは異常終了してしまいます.

例外処理を使えば,本来の処理とエラーをはじめとする異常処理をきれいに分離することができます.

1.3.8 意味が明確なキャスト演算子

明示的な型変換のためのキャストは,Cでもよく使われます.しかし,1種類しかないCのキャストは,万能すぎて多くの危険を伴います.また,「(型)」という構文は検索しにくいため,危険がありそうなキャストを洗い出すのは容易ではありません.

C++では,主に互換性のために,Cと同じスタイルのキャストを使うこともできます.しかし,特に事情がないかぎり(たとえば,Cと共通のヘッダファイルの中で定義するマクロで使うなど),何をしたいのかをより明確に表現できるC++固有のキャスト演算子を使用すべきです.C++固有のキャスト演算子には,static_cast,const_cast,reinterpret_cast,およびdynamic_castがあります.このうち,dynamic_castは第2章で解説するので,ここでは取り上げません.

C++固有のキャスト演算子は,次のような構文で記述します.

【C++固有のキャストの記述】

int value = static_cast<int>(123.456);

この例では,double型のリテラルをint型にキャストしています.どのキャスト演算子も,次の形式になります.

【C++固有のキャストの記述の構文】

キーワード<変換後の型>(オペランド)

それでは,それぞれのキャスト演算子を詳しく見ていきましょう.

static_cast

「static_cast」は,文字どおり静的なキャストを行います.static_castで型変換を行うことができるのは,変換前と変換後の型の,どちらか一方から他方への暗黙的な型変換が可能な場合に限られます.ただし,static_castによってcv修飾子(const修飾子とvolatile修飾子)を外すことはできません.

void* p;
const int* q;
void func();
int main()
{
    int a;
    double b;
    a = static_cast<int>(b);        // ← OK! 
    b = static_cast<double>(a);     // ← OK! 
    a = static_cast<int>(p);        // ← エラー! 
    p = static_cast<void*>(a);      // ← エラー! 
    b = static_cast<double>(p);     // ← エラー! 
    p = static_cast<void*>(b);      // ← エラー! 
    p = static_cast<void*>(&func);  // ← エラー! 
    q = static_cast<const int*>(p); // ← OK! 
    p = static_cast<void*>(q);      // ← エラー! 
    return 0;
}

static_castを使えば,オブジェクト型へのポインタ型と関数型へのポインタ型の間での型変換,整数型とポインタ型の間での型変換,cv修飾子を外す型変換といった,強引なキャストはすべてコンパイルエラーになります.これによって,危険極まりない強引なキャストを大幅に回避することができます.

const_cast

「const_castは,cv修飾子(const修飾子とvolatile修飾子)を取り除くための専用のキャスト演算子です.const_castを使うことで,ポインタ型や参照型の参照先の型のcv修飾子を取り除くことができますが,それ以外の型変換は行うことができません.ただし,もともと暗黙的な型変換が可能な場合には,その限りではありません.

const int* p;
int* q;
void* r;
int main()
{
    q = const_cast<int*>(p);  // ← OK! 
    r = const_cast<char*>(p); // ← エラー! 
    q = const_cast<int*>(r);  // ← エラー! 
    return 0;
}

const_castを使用するような状況は,多くの場合,何かが間違っています.const_castの使用は,本来cv修飾子の使用を徹底すべきであるにもかかわらず,それを怠っている既存の関数や構造体などを使用する場合に限るべきです.

reintepret_cast

reinterpret_castは,整数型とポインタ型との間の型変換,異なる型へのポインタ型どうしの型変換,異なる型への参照型どうしの型変換といった,いわゆる強引な型変換のためのキャスト演算子です.強引な型変換のためのreinterpret_castですが,cv修飾子(const修飾子とvolatile修飾子)を取り除くためには,やはりconst_castを使わなければなりません.

reinterpret_castという長く汚いキーワードは,キーボードでそれを入力している間に,この演算子を本当に使用すべきかどうかプログラマーに再考させるため,あえてこのような名前になっています.また,ソースコードを一見しただけで,この汚いキーワードはすぐに目に飛び込んできます(最近のエディタは,キーワードに色を付けてくれることが多いので,なおさらよく目立ちます).