C++編【言語解説】 第24章 入れ子クラスとローカルクラス

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

この章の概要

この章の概要です。

入れ子クラス

クラス定義の内側で定義されたクラスを、入れ子クラス(nested class、ネストされたクラス、メンバクラス)と言います。
C++ では、クラスと構造体はほぼ同一の概念なので(第12章)、 この先の話題は構造体にも当てはまります。

入れ子クラスは、1つのスコープを形成します。 そのため、入れ子クラスにアクセスするための完全な表記は、 「Outer::Inner」のように、外側のクラス名と入れ子クラス名とを、スコープ解決演算子でつなげたものになります。
ただし、名前空間や静的メンバの場合と同様、すでに Outer のスコープ内にいる場合は「Outer::」の部分を省略できます。

C言語でも、構造体を入れ子にすることができますが、内側の構造体は新たなスコープを形成しません(C言語編第26章)。

また、入れ子クラスを記述する位置に応じて、アクセス指定子も通常通り効力を持ちます。 つまり、入れ子クラスを「公開」すれば、外部から使用できますし、 「非公開」とすれば、外側のクラス内部でしか使用できなくなります。

先ほど、入れ子クラスがスコープを形成すると書きましたが、 その点では、Outer名前空間の内側に Innerクラスを定義すれば、「Outer::Inner」という形になるので、同じことだと言えます。 しかし、名前空間の場合は、アクセス制御ができないのに対し、 入れ子クラスの場合は、アクセス制御が可能であるという点で、大きな違いがあります。
この違いは、あるクラスの内部実装のために新たなクラスを導入したいというケースで、 「非公開」な入れ子クラスを定義するという方法で活用できます。 入れ子クラスを使わないとすれば、Outerクラスの外側に出すしかないので、 他の場所からもアクセスでき、無用な依存関係を発生させてしまいます。

#include <cassert>
#include <iostream>
#include <string>

#define SIZE_OF_ARRAY(array) (sizeof(array)/sizeof(array[0]))

class Student {
private:
	class Score {
	public:
		Score(int japanese, int math, int english);

	public:
		// 平均点を返す
		int GetAverage() const;

	private:
		enum Subject {
			SUBJECT_JAPANESE,
			SUBJECT_MATH,
			SUBJECT_ENGLISH,

			SUBJECT_NUM,  // 総数を表すダミー
		};

	private:
		int  mScores[SUBJECT_NUM];
	};

public:
	Student(const std::string& name, int japanese, int math, int english) :
		mName(name),
		mScore(japanese, math, english)
	{}

public:
	inline const std::string& GetName() const
	{
		return mName;
	}
	
	// 平均点を返す
	inline int GetAverage() const
	{
		return mScore.GetAverage();
	}

private:
	const std::string  mName;
	const Score        mScore;
};

Student::Score::Score(int japanese, int math, int english)
{
	mScores[SUBJECT_JAPANESE] = japanese;
	mScores[SUBJECT_MATH]     = math;
	mScores[SUBJECT_ENGLISH]  = english;
}

int Student::Score::GetAverage() const
{
	int sum = 0;
	for (int i = 0; i < SIZE_OF_ARRAY(mScores); ++i) {
		sum += mScores[i];
	}
	return sum / SIZE_OF_ARRAY(mScores);
}


int main()
{
	Student student("Tanaka Miki", 92, 66, 75);

	std::cout << "Name: " << student.GetName() << "\n"
	          << "  Average: " << student.GetAverage() << std::endl;
}

実行結果:

Name: Tanaka Miki
  Average: 77

生徒の科目毎の得点を管理する Scoreクラスを、「非公開」の入れ子クラスにしています。

このサンプルでは、Score入れ子クラスには、GetAverage という、平均点を返すメンバ関数しか公開されていませんが、 「一番成績が良かった(悪かった)科目と得点を返す」といった関数を追加することが考えられます。
このように、成績に関する機能を1つのクラスとしてまとめることで、処理の一元化を図ることができます。


次に、入れ子クラスを「公開」する使い方です。

#include <cassert>
#include <iostream>
#include <string>

#define SIZE_OF_ARRAY(array) (sizeof(array)/sizeof(array[0]))

class Student {
public:
	class Score {
	public:
		Score(int japanese, int math, int english);

	public:
		// 平均点を返す
		int GetAverage() const;

	private:
		enum Subject {
			SUBJECT_JAPANESE,
			SUBJECT_MATH,
			SUBJECT_ENGLISH,

			SUBJECT_NUM,  // 総数を表すダミー
		};

	private:
		int  mScores[SUBJECT_NUM];
	};

public:
	Student(const std::string& name, const Score& score) :
		mName(name),
		mScore(score)
	{}

public:
	inline const std::string& GetName() const
	{
		return mName;
	}

	inline const Score& GetScore() const
	{
		return mScore;
	}

private:
	const std::string  mName;
	const Score        mScore;
};

Student::Score::Score(int japanese, int math, int english)
{
	mScores[SUBJECT_JAPANESE] = japanese;
	mScores[SUBJECT_MATH]     = math;
	mScores[SUBJECT_ENGLISH]  = english;
}

int Student::Score::GetAverage() const
{
	int sum = 0;
	for (int i = 0; i < SIZE_OF_ARRAY(mScores); ++i) {
		sum += mScores[i];
	}
	return sum / SIZE_OF_ARRAY(mScores);
}


int main()
{
	Student::Score score(92, 66, 75);
	Student student("Tanaka Miki", score);

	std::cout << "Name: " << student.GetName() << "\n"
	          << "  Average: " << student.GetScore().GetAverage() << std::endl;
}

実行結果:

Name: Tanaka Miki
  Average: 77

Score入れ子クラスが「公開」されているので、Studentクラスの外部でインスタンス化することができます。 そのため、外部で成績に関するデータを作り、Studentクラスに渡すようにすることも可能です。


入れ子クラスからは、外側のクラスの静的なメンバへアクセスできます。 そのメンバが「非公開」であっても問題ありません。 例えば、typedef された名前や、enum などへもアクセスできます。
例として、ここまでに挙げた Score入れ子クラスで定義されていた Subject列挙型を、Studentクラスの方へ移動させてみます (クラス定義の部分だけを掲載します)。

class Student {
private:
	enum Subject {
		SUBJECT_JAPANESE,
		SUBJECT_MATH,
		SUBJECT_ENGLISH,

		SUBJECT_NUM,  // 総数を表すダミー
	};

public:
	class Score {
	public:
		Score(int japanese, int math, int english);

	public:
		// 平均点を返す
		int GetAverage() const;

	private:
		int  mScores[SUBJECT_NUM];
	};

public:
	Student(const std::string& name, const Score& score) :
		mName(name),
		mScore(score)
	{}

public:
	inline const std::string& GetName() const
	{
		return mName;
	}
	
	inline const Score& GetScore() const
	{
		return mScore;
	}

private:
	const std::string  mName;
	const Score        mScore;
};

Score入れ子クラス内で SUBJECT_NUM を使用していますが、問題なくコンパイルできます。

一方で、外側のクラスのオブジェクトが特定できないので、静的でないメンバへはアクセスできません。 例えば、Score入れ子クラス内から、生徒の名前 (Student::mName) をアクセスすることはできません。 これを解決したければ、外側のクラスのオブジェクトのポインタや参照を、入れ子クラスに渡してやり、それを経由させるしかありません。
Score入れ子クラスに、生徒の名前と得点の一覧を出力させる Printメンバ関数を追加するとします。

#include <cassert>
#include <iostream>
#include <string>

#define SIZE_OF_ARRAY(array) (sizeof(array)/sizeof(array[0]))

class Student {
private:
	enum Subject {
		SUBJECT_JAPANESE,
		SUBJECT_MATH,
		SUBJECT_ENGLISH,

		SUBJECT_NUM,  // 総数を表すダミー
	};

public:
	class Score {
	public:
		Score(Student* student, int japanese, int math, int english);

	public:
		// 平均点を返す
		int GetAverage() const;

		void Print() const;

	private:
		Student*  mStudent;
		int       mScores[SUBJECT_NUM];
	};

public:
	Student(const std::string& name, int japanese, int math, int english) :
		mName(name),
		mScore(this, japanese, math, english)
	{}

public:
	inline const std::string& GetName() const
	{
		return mName;
	}

	inline const Score& GetScore() const
	{
		return mScore;
	}

private:
	const std::string  mName;
	const Score        mScore;
};

Student::Score::Score(Student* student, int japanese, int math, int english) :
	mStudent(student)
{
	mScores[SUBJECT_JAPANESE] = japanese;
	mScores[SUBJECT_MATH]     = math;
	mScores[SUBJECT_ENGLISH]  = english;
}

int Student::Score::GetAverage() const
{
	int sum = 0;
	for (int i = 0; i < SIZE_OF_ARRAY(mScores); ++i) {
		sum += mScores[i];
	}
	return sum / SIZE_OF_ARRAY(mScores);
}

void Student::Score::Print() const
{
	std::cout << "Name: " << mStudent->GetName() << "\n"
	          << "  Japanese: " << mScores[SUBJECT_JAPANESE] << "\n"
	          << "      Math: " << mScores[SUBJECT_MATH] << "\n"
	          << "   English: " << mScores[SUBJECT_ENGLISH] << std::endl;
}


int main()
{
	Student student("Tanaka Miki", 92, 66, 75);

	student.GetScore().Print();
}

実行結果:

Name: Tanaka Miki
  Japanese: 92
      Math: 66
   English: 75

Score入れ子クラスの中から、Student::mName や Student::GetName() へはアクセスできませんが、 Studentクラスのインスタンスをポインタや参照で渡しておき、それを経由させれば、アクセスできます。

この形の場合、Score入れ子クラスのインスタンスを外部で作ろうとすると、Studentクラスのインスタンスが必要になってしまいます。 成績データを使って Studentインスタンスを作ろうとしていることを考えると、順序がおかしくなります。

ローカルクラス

クラスの定義は、関数内(メンバ関数内も含む)でも行うことができます。 このようなクラスはローカルクラスと呼ばれ、その関数内でのみ使用できます。

ローカルクラスは、関数内だけにスコープを限定できるので、ある関数の実装のためだけに必要な処理をうまく局所化できます。

#include <iostream>

int main()
{
	class Printer {
	public:
		Printer() :
			mBegin(""), mEnd("")
		{}

		void SetQuote(const char* begin, const char* end)
		{
			mBegin = begin;
			mEnd = end;
		}

		void Puts(const char* str)
		{
			std::cout << mBegin << str << mEnd << std::endl;
		}

	private:
		const char* mBegin;
		const char* mEnd;
	};

	Printer printer;

	printer.Puts("Test Message1");

	printer.SetQuote("[[", "]]");
	printer.Puts("Test Message2");
	printer.Puts("Test Message3");
}

実行結果:

Test Message1
[[Test Message2]]
[[Test Message3]]


ローカルクラスには、以下のように様々な制約があります。

C++11

C++11 では、ローカルクラスを、テンプレート引数に使用できるようになりました。

VisualC++ 2013/2015/2017、clang 3.7 は、いずれも対応しています。

また、制約と呼ぶのがふさわしいかどうか分かりませんが、 ローカルクラスのメンバ関数の定義は、ローカルクラスの定義の内側に書くしかありません。 そのため、必然的にインライン化の指定をしたことになります第12章)。

また、ローカル「クラス」だからといって、メンバ変数を持たせないといけない訳ではないので、 メンバ関数だけを用意して、ローカル「関数」のような感じで使うことも可能です。 C++ には、ローカル関数は無いですが、こうして代用することはできます。


練習問題

問題@ Studentクラスと Score入れ子クラスの例を拡張して、その生徒の各学期末(1学期、2学期、3学期)の成績を管理するようにして下さい。 各教科の得点は、以下のように与えられているものとします。

const Student::Score SCORES[] = {
	Student::Score(90, 60, 71),
	Student::Score(84, 70, 65),
	Student::Score(92, 67, 73),
};

問題A Printerローカルクラスの例を変形して、ローカル関数のような使い方をするプログラムを作成して下さい。


解答ページはこちら

参考リンク

更新履歴

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

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

'2016/1/9 新規作成。



前の章へ

次の章へ

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

Programming Place Plus のトップページへ