C++編【言語解説】 第18章 const の活用

先頭へ戻る

このエントリーをはてなブックマークに追加

この章の概要

この章の概要です。

constオブジェクト

この章では、const修飾子(以降、単に const と記述します)とそれに関連する話題を取り上げます。 C++ において、const の利用価値は非常に高く、使える場面では積極的に使うべきです

まずは、最も単純と思われる使い方から見ていきましょう。

int main()
{
	const int a = 10;

	a = 20;  // エラー
}

変数a は const付きですから、初期化した後は、新たな値で上書きすることができなくなります。 要するに、const を付けることによって、初期化は許すが、代入は許可しないと指定したことになります。

この使い方は、C言語のときからお馴染みだと思いますが、C言語と C++ とでは違いがあります。

int main()
{
	const int a = 10;
	int array[a];  // C言語ではエラー、C++ では OK
}

C言語で const を付けて定義された変数は、書き換えができない変数という扱いであり、定数としては使えません。 そのため、配列の要素数のように、定数を要求する場面には使用できません。


int型のような組み込み型だけでなく、クラスのオブジェクトであっても同様です。 const の付いたオブジェクトの場合、書き換えが起こらないことを保証するため、「公開」されているメンバであっても、 メンバ変数を書き換えることはできませんし、後述する constメンバ関数でないメンバ関数を呼び出すこともできません。

内部結合と外部結合

グローバルな constオブジェクトが内部結合になるのか、外部結合になるのかのルールが、C言語と C++ とで異なっています。

C言語の場合、グローバル変数は、static修飾子を付ければ内部結合になりますが、付けなければ外部結合になります。 一方、C++ の場合は、extern修飾子を付ければ外部結合になり、付けなければ内部結合になります。

プログラム全体で使いたいような定数を、ヘッダファイルに置くとき、 C++ であれば、ソースファイル側に定義を書き、ヘッダファイル側に extern修飾子付きの宣言を書くのが良いです。

// data.cpp
#include "data.h"

const int DATA_VALUE = 100;
// data.h
#ifndef DATA_H
#define DATA_H

extern const int DATA_VALUE;

#endif
// main.cpp
#include <iostream>
#include "data.h"

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

実行結果:

100

このようにすると、data.h を複数の箇所で include したとしても、実体は data.cpp にある1つだけになるので、 プログラムサイズの増加を防げます。 また、後で初期値を変更したとしても、data.h を修正する必要がないので、data.cpp 以外には再コンパイルの必要もありません。

constメンバ変数

メンバ変数を const にすることもできます。 この場合、メンバイニシャライザ(第13章)を使って初期化する必要があり、 たとえコンストラクタ内であっても代入は許されません。

class MyClass {
public:
	MyClass() : a(10) {}
	
private:
	const int a;
};

constメンバ変数は、コンストラクタの引数で外部から与えられた情報を、 うっかり変更してしまわないように、大切に取っておくために利用できます。

C++11 (非静的メンバ変数の定義箇所での初期化)

第13章でも取り上げていますが、 C++11 の場合、静的メンバ変数でなければ、メンバ変数の定義時に初期値を与えられますから、 const のメンバ変数の初期値を、定義時に与えることもできます。

class MyClass {
public:
	MyClass() {}
	
private:
	const int a = 10;
};

この機能は、VisualC++、Xcode のいずれでも使用できます。

constメンバ関数

メンバ関数の宣言のとき、後ろに const を付けることによって、constメンバ関数を作ることができます。 これは、第12章で getter の説明をしたときにも登場しました。

class Student {
public:
	inline void SetScore(int score)
	{
		mScore = score;
	}
	inline int GetScore() const  // constメンバ関数
	{
		return mScore;
	}
	
private:
	int          mScore;  // 得点
};

あるオブジェクトの実体、参照、ポインタからメンバ関数を呼び出すとき、 const が付いている場合は、constメンバ関数しか呼び出せません
例えば、前章で見たように、 一時オブジェクトは const参照で束縛できますが、const参照からメンバ関数を呼び出すなら、constメンバ関数でなければいけません。

単純な、よくある説明の仕方をすれば、constメンバ関数内ではメンバ変数を書き換えることが禁止されます。 もう少し実際の挙動に即して言えば、constメンバ関数内では、thisポインタが constポインタになるということです。 「this->mScore = 10;」のように書いたとき、this が constポインタならば、ポインタが指し示す先にあるものを書き換えられないというC言語のルールがあるので、 結果的にメンバ変数の書き換えはできないということになります。
たとえ、thisポインタの部分を省略して「mScore = 10;」と書いたとしても、 これは、「this->」の部分の記述を省略しているだけであって、本質は何も変わりません。

また当然ながら、constメンバ関数から、別のメンバ関数を呼び出す場合、呼び出す先も constメンバ関数でなければなりません。 これも、this が constポインタになっているのだと知っていれば、納得がいくでしょう。

なお、静的メンバ関数には const を付加することはできません

constメンバ関数と、非constメンバ関数とは、オーバーロードできます
もし、constオブジェクトから呼び出そうとすれば、constメンバ関数の方が選択されますし、 非constオブジェクトから呼び出そうとすれば、非constメンバ関数の方が選択されます。

constオブジェクト、非constオブジェクトを問わずに呼び出せるようなメンバ関数が必要であれば、 constメンバ関数と、非constメンバ関数とでオーバーロードしなければなりません。 このような場合、constメンバ関数と、非constメンバ関数の実装が同じになってしまうことがあります。 まったく同じコードを2か所に書くのは保守面から望ましくないので、次のように書くと良いです。

class MyClass {
public:
	inline int& Get()
	{
		return const_cast<int&>(static_cast<const MyClass*>(this)->Get());
	}
	
	const int& Get() const;
};

const int& MyClass::Get() const
{
	// 実装を書く
}

実装は、constメンバ関数の方だけに書き、非constメンバ関数は constメンバ関数の方を呼び出す形にします。 constメンバ関数の方に書くのは、制約が強い側で書いた方が、コンパイラのチェックが入り安全だからです。 逆にしてしまうと、const制が台無しになります。

非constメンバ関数から、constメンバ関数を呼び出すには工夫が必要です。 同じ名前、同じ引数の関数ですから、単純に書くと、自分自身を呼び出して無限再帰してしまいます。
そこでまず、thisポインタに明示的に const を付けます。 そのためには、「static_cast<const MyClass*>(this)」というように、static_cast を使います
const付きの thisポインタが手に入れば、これを経由して関数を呼び出せば、 constポインタ経由なら constメンバ関数の方が選択されるルールによって、 意図通り、constメンバ関数版の Get関数を呼び出すことができます。

最後に、constメンバ関数が返した戻り値を、const_cast で const を取り除いて返します。 const_cast は使わないことが望ましいですが、この場面では、constメンバ関数内で何をしているか、 プログラマ自身で分かっている訳ですし、呼出し元はそもそも非const版なのだから、 書き換えられるようにすることにも問題はありません。

mutableキーワード

constメンバ関数は非常に重要かつ有用な機能ですが、稀に、思うように使えない場面があります。

例えば、結果をキャッシュしておくように実装する場合が挙げられます。

class Accessor {
public:
	const char* Get() const;
	
private:
	const char* mData;
};

const char* Accessor::Get() const
{
	if (mData == NULL) {
		mData = new char[1 * 1024 * 1024];  // constメンバ関数内ではメンバ変数を書き換えられない。
	}
	return mData;
}

Accessor::Get関数は、返すべきデータが既に存在していれば、単にそのデータのアドレスを return するだけです。 この形であれば constメンバ関数にできます。 この章の冒頭で書いた通り、const が使えるのなら積極的に使うべきですし、 getter系のメンバ関数は、constオブジェクトからでも呼び出せた方がいいでしょうから、constメンバ関数にしたいところです。

しかし、return するデータを作り出すこと自体が、非常にコストが掛かるものであり (例えば、ネットワークを通してデータを取得してこなければならないだとか、単に非常に巨大であるだとか)、 また、条件分岐の仕方によっては、そのデータが使われないこともあるのであれば、 初めて Accessor::Get関数が呼び出されたときにだけ、データを作ることが望ましいでしょう。
そのような要件で実装する場合、Accessor::Get関数の中でデータを作りたいところですが、 そうすると、メンバ変数 mData を書き換える必要が出てくるため、constメンバ関数にできなくなってしまいます。

この問題を解決するには、普通に考えれば、次のようにメンバ関数を分離するしかありません。

class Accessor {
public:
	void CreateData();
	const char* Get() const;
	
private:
	const char* mData;
};

void Accessor::CreateData()
{
	if (mData == NULL) {
		mData = new char[1 * 1024 * 1024];
	}
}

const char* Accessor::Get() const
{
	return mData;
}

これがダメだという訳ではありませんが、このクラスの利用者側としては手順や注意点が増えることになります。 Accessor::CreateData関数を呼び忘れたらどうなるのか、呼んではいたが失敗していたらどうなるのかといった問題があります。

そこで、1つの手段として、constメンバ関数からでもメンバ変数を書き換えられるようにする機能が使えます。 そのためには、constメンバ関数から書き換えても構わないことを表す mutableキーワードを、 メンバ変数の宣言時に付加します。

class Accessor {
public:
	const char* Get() const;
	
private:
	mutable const char* mData;  // constメンバ関数からでも書き換えてよい
};

const char* Accessor::Get() const
{
	if (mData == NULL) {
		mData = new char[1 * 1024 * 1024];  // OK
	}
	return mData;
}

mutableキーワードは、その存在意義を正しく理解していないと、誤った使い方をしてしまうでしょう。 そもそも、constメンバ関数は、メンバ変数を書き換えられないようにすることで、オブジェクトの状態を変更しないことが本質です。 mutable は明らかにその本質に反しています。

これは、「オブジェクトの状態を変更しない」という部分をどのように捉えるかが重要です。 本当に、「1ビットも書き換えないもの」だと捉えるのであれば、mutable は決して使ってはいけません
しかし、「クラスの外から変更の有無が分からなければ、こっそり書き換えられていても構わない」と考えるのなら、 mutable は(使い方を誤らなければ)合法です。 合法になる例としては、関数を呼び出した回数をカウントしておくだとか、 計算結果をキャッシュしておいて、次回以降は計算をスキップして結果だけ返すようにするといったものがあります。

C++11 (constexpr)

C++11

C++11 には、新たなキーワード constexpr が追加されています。 このキーワードを使える箇所は複数ありますが、コンパイル時に評価できる定数式を作るために使われます。 簡単に言えば、ただの値を定数とするのではなく、式によって定数を決定できるということです。

まず、const と比較してみます。

#include <iostream>

int main()
{
	int x = 0;             // 定数式でない

	const int a = 10;
	const int b = a;
	const int c = x;       // const では許される

	constexpr int aa = 10;
	constexpr int bb = aa;
//	constexpr int cc = x;  // x は定数式でないのでエラー

	constexpr int dd = a;  // OK
//	constexpr int ee = c;  // c は(c を初期化している x が)定数式でないのでエラー
}

constexpr を付けて定義される変数は、その初期値をコンパイル時に決定できなければなりません。 「10」のようなリテラル値や、constexpr の付いた他の定数を使って初期化することができます。
上の例で、コメントアウトされている2つの方法ではコンパイルエラーになります。 cc は x で初期化しようとしていますが、x はただの変数であり、コンパイルの時点で値が確定していないのでエラーになります。
ee は c で初期化しようとしてます。 c は const付きですが、その c の初期値である x が定数でないため、 結局 cc のときと同様、コンパイルの時点で初期値が確定できずエラーになります。


constexpr は、関数に付けることもでき、constexpr関数と呼びます。 constexpr関数は、実引数がすべてコンパイル時に確定できる場合、関数呼び出し全体を定数式として扱えるようになります。 また、constexpr関数の実引数がコンパイル時に確定できない場合、単に、通常の関数のように振る舞います。
コンパイル時に確定できればいいので、関数テンプレートを constexpr関数にすることも可能です。

constexpr関数は、ある程度複雑な計算を含んでいても、コンパイル時点でその計算処理を終えられるので、 実行時のパフォーマンスを大幅に向上できる可能性を秘めており、非常に強力な機能と言えます。 ただ、以下のように制約も多くなっています。

実例は省きますが、if や switch が使えないことは ?: で代用できますし、ループの代わりに再帰関数呼び出しで代用できます。

C++14 では、大幅に制約が解消されており、ローカル変数、if/switch、ループも使用できます。 また、複数の return文を含むことも可能になりました。

実際の使用例は次のようになります。

#include <iostream>

constexpr int func(int a, int b)
{
	return a * 10 + b;
}

int main()
{
	constexpr int a = func(3, 5);  // constexpr関数は constexpr の定数の初期化に使える

	int x = 7;
	int b = func(a, x);  // constexpr関数の実引数がコンパイル時に確定できなくても、コンパイル可能

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

実行結果:

35
357

メンバ関数を constexpr関数にすることもできます。 この場合、暗黙的に constメンバ関数になるので、宣言の後ろに付ける constキーワードは不要です。

class MyClass {
public:
	inline constexpr int GetMaxValue()  // constexprメンバ関数
	{
		return 100;
	}
};

C++14 では、このルールは撤廃されており、暗黙的に constメンバ関数にはなりません。

上のコラムにあるように、C++14 でルールが変更された関係で、 VisualC++ 2015 や Xcode 8.3.3 では、constexprメンバ関数に const を付加しないと warning が出ます。


コンストラクタに constexpr を付けると、そのクラスをリテラル型として使用できるようになります。 リテラル型のクラスのオブジェクトは、定数式として使用できます。

#include <iostream>

class MyClass {
public:
	constexpr MyClass(int max) : mMaxValue(max)
	{}

	inline constexpr int GetMaxValue()
	{
		return mMaxValue;
	}

private:
	const int mMaxValue;
};

constexpr int func(MyClass a)
{
	return a.GetMaxValue() * 10;
}

int main()
{
	MyClass a = 10;

	std::cout << a.GetMaxValue() << "\n"
	          << func(a) << std::endl;
}

実行結果:

10
100

constexpr は、VisualC++ 2013 では対応しておらず、VisualC++ 2015/2017 は対応しています。 Xcode は対応されています。


練習問題

問題@ 次のようなメンバ関数は適切と言えるでしょうか?

class DataStore {
public:
	inline int* GetValue() const
	{
		return &mValue;
	}
	
private:
	int    mValue;
};

問題A 次のクラスの例で、3つの目的の同じメンバ関数の実装について、それぞれの利点や欠点を挙げて下さい。

class Student {
public:
	
	// 実装1 (constポインタで返す)
	inline const std::string* GetName1() const
	{
		return &mName;
	}
	
	// 実装2 (const参照で返す)
	inline const std::string& GetName2() const
	{
		return mName;
	}

	// 実装3 (実体で返す)
	inline const std::string GetName3() const
	{
		return mName;
	}

private:
	std::string    mName;
};


解答ページはこちら

参考リンク

更新履歴

'2017/7/30 clang 3.7 (Xcode 7.3) を、Xcode 8.3.3 に置き換え。

'2017/3/25 VisualC++ 2017 に対応。

'2016/10/15 clang の対応バージョンを 3.7 に更新。

'2015/10/24 「C++11 (constexpr)」の項を追加。

'2015/10/12 clang の対応バージョンを 3.4 に更新。

'2015/9/5 VisualC++ 2012 の対応終了。

'2015/8/18 VisualC++2010 の対応終了。

'2015/8/15 VisualC++ 2015 に対応。

'2014/10/18 clang 3.2 に対応。

'2014/9/22 新規作成。



前の章へ

次の章へ

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

Programming Place Plus のトップページへ