【C++】const の扱いが複雑すぎる

2020/08/05 間違っていた箇所を修正。

本稿では、C++ の言語仕様で決められている const の使い方について解説します。

const の運用面の問題については、以下の記事にまとめています。

動作確認には、Microsoft Visual Studio 2017 を使用しています。
gcc や clang などのコンパイラでは動作が変わる可能性があります。

const の付け方には意味がある

const は変数名またはポインタ型の場合は * の前と後に付けることができ、前に付けるか、後に付けるか、変数がポインタ型かどうかで意味が変わります。

const_cast を使って、変数の const を付けたり外したりできますが、const_cast は非常に危険なものです。

const はメンバ関数にも付けることができ、メンバ変数と併用する場合に注意が必要です。

C++ で const を使うには、これらについて理解している必要があります。
これらを整理すると、以下の見出しにまとめることができます。

以下で、それぞれの見出しに沿った内容を説明します。

非ポインタ型変数の const

非ポインタ型かポインタ型かに関わらず、const を型名の前に付けても後ろに付けても意味は同じです。
const を付けると、変数の値を変更できなくなります。

const int hoge = 1; //前 const
int const moge = 2; //後 const

hoge = 3; //コンパイルエラー
moge = 4; //コンパイルエラー

非ポインタ型の場合、const は型名の前か後のどちらか一方にだけ付けることができます。
型名の前後に const を付けるとコンパイルエラーになります。

const int const hoge = 5; //コンパイルエラー

ポインタ型変数の const

ポインタ型変数では、const の付け方が以下の3パターンあり、それぞれ意味が変わります。

1. 前に付ける
2. 後に付ける
3. 前後に付ける

どこを基準にした前と後なのか?

というと、ポインタ型の場合、* の前か、後か?で判断します。
型名の前後ではありません。

前に付ける

const int* hoge = new int(1);
// int const* hoge = new int(1); と書いても意味は同じ

*hoge = 2; //コンパイルエラー

int moge = 3;

hoge = &moge; //OK

前 const は、アドレスに入っている値の書き換えができなくなります。
アドレスを書き換えることはできます。

後に付ける

int* const hoge = new int(1);

*hoge = 2; //OK

int moge = 3;

hoge = &moge; //コンパイルエラー

後 const は、アドレスの書き換えができなくなります。
アドレスに入っている値の書き換えはできます。

前後に付ける

const int* const hoge = new int(1);
// int const * const hoge = new int(1); と書いても意味は同じ

*hoge = 2; //コンパイルエラー

int moge = 3;

hoge = &moge; //コンパイルエラー

前後 const は、前 const と後 const の両方です。
アドレスに入っている値の書き換えができず、アドレスの書き換えもできません。

const の位置が違うだけなのに、全て別の型として扱われる

const int* または int const*
int* const
const int* const または int const* const

これらは全て、別の型です。
前 const に、後 const を代入する場合、const_cast を使ってキャストする必要があります。

ただ、この const_cast は正しく機能することが保証されていません。
チームによっては、const_cast の使用を禁止することもあります(そういうルールを規定しているチームで開発した経験があります)。

const_cast について
const_cast しても良い例

const を付けていない変数に、const_cast で const を付けるのは問題ありません。

int hoge = 1;
const int moge = const_cast<const int>(hoge);

const_cast できるか保証がない例

const が付いている変数から、const_cast で const を付けたり外したりするのは未保障です。
私は const_cast は const 外しに使うと覚えたのですが、間違っていました。

const_cast に関する情報が以下のページに書かれています。
こちらの情報にどれだけ信ぴょう性があるのか?は不明ですが、const_cast の扱いには十分に注意した方が良いという点は変わりません。

リリースビルドで、いついかなる状況でも正しくキャストできることを確認できれば使えます。
ただ、「いついかなる状況でも正しくキャストできることを確認する」というのは悪魔の証明(絶対に証明できないこと)です。
そう考えると、const_cast 禁止というルールを設けて開発するのも分かります。

メンバ関数の const

メンバ関数にも const を付けることができます。
ここから少し複雑になります。

class test {
public:
	const char* GetHoge() const {
		return const_cast<const char*>(this->hoge);
	}
private:
	char* hoge = new char[5]{'H','O','G','E','\0'};
}

ポインタ型プライベートメンバ変数の hoge には const が付いていません。
hoge のアドレスとアドレスに入っている値を書き換えて欲しくない場合、const を戻り値(char*)の前後に付ける必要があります。
ただし、メンバ関数名の後ろに const を付けた場合、メンバ変数の値の書き換えができなくなります。
ポインタ型メンバ変数の場合、値はアドレスなので、アドレスの書き換えができないことになります。

つまり

戻り値の型 メンバ関数名(引数リスト) const

と宣言した場合、

このメンバ関数内では、メンバ変数の書き換えができない。
メンバ変数がポインタ型の場合は、アドレスを変更できない。

ということになります。

戻り値として返されるのは const char* つまり、前 const なので、値の書き換えができません。
値もアドレスも書き換えできないようにするためには、戻り値の前後に const を付ける必要があるように思いますが、メンバ関数に付いている const と意味が重複します。

メンバ関数に const を付けるべきなのか?
ポインタ型戻り値の前後に const を付けるべきなのか?

という疑問が生まれます。
この場合、どちらが正しいのでしょうか?

答えは、場合による

どちらかが正しく、どちらかが間違っている…という単純な話ではありません。

以下で、それぞれについて説明します。

メンバ関数名の後ろに const を付けるということ

このメンバ関数内では、メンバ変数を絶対に書き換えない!
と宣言するための行為です。
書き換えるのであれば、メンバ関数名の後ろに const を付けることはできません。

以下の場合は成立しません。

class test {
public:
	// 以下のメンバ関数はコンパイルエラーになる
	const char* GetHoge() const {
		this->moge++; //メンバ変数を書き換えている
		return const_cast<const char*>(this->hoge);
	}
private:
	char* hoge = new char[5]{'H','O','G','E','\0'};
	int moge = 0;
}
ポインタ型戻り値の前後に const を付けるということ

メンバ変数の書き換えをするけど、戻り値は絶対に書き換えさせない!
と宣言するための行為です。

前述のソースコードは、以下のように書けば成立します。

class test {
public:
	// メンバ変数を書き換えるが、戻り値は書き換え不可にする
	const char* const GetHoge() {
		this->moge++; //メンバ変数を書き換える
		return const_cast<const char* const>(this->hoge);
	}
private:
	char* hoge = new char[5]{'H','O','G','E','\0'};
	int moge = 0;
}
まとめ
  • 非ポインタ型変数はどっちにつけても意味は同じ。
  • ポインタ型変数の前 const は値、後 const はアドレス、前後 const は両方の書き換えを禁止する。
  • const_cast はコンストなし変数で使う場合だけ安全。
  • メンバ関数名の後 const を付けるべきか、戻り値に const を付けるべきかは場合による。
  • ポインタ型戻り値の後 const とメンバ関数名の後 const は意味が重複するので、両方つけるのは無意味。

C++ は、たかが const ひとつをとっても、これだけのことを気にしなければならない言語なので、上流開発で C++ を使うのをやめたいです。
これからゲーム開発の技術を身に着けようとする人達にとって、学習コストが高すぎます。

そうは言っても、二大ゲーム開発エンジンの片翼である Unreal Engine が C++ を採用してるので、ゲームの上流開発でも、まだまだ付き合わされることになりそうです。
ソニーが Unreal Engine の開発元である Epic Games の持ち株を保有する形でタッグを組んだので、もうすぐ発売される PS5 のゲーム開発では Unreal Engine が主流になる(UEを使って開発する案件が増える)のではないかと予想しています。
ほぼ大手の下請けがメインである中小ベンチャー企業にとっては、Unreal Engine を使った開発ノウハウの習得は避けて通れない道になりますね。

 

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です