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

トップページC++編

C++編で扱っている C++ は 2003年に登場した C++03 という、とても古いバージョンのものです。C++ はその後、C++11 -> C++14 -> C++17 -> C++20 -> C++23 と更新されています。
なかでも C++11 での更新は非常に大きなものであり、これから C++ の学習を始めるのなら、C++11 よりも古いバージョンを対象にするべきではありません。特に事情がないなら、新しい C++ を学んでください。 当サイトでは、C++14 をベースにした新C++編を作成中です。

この章の概要 🔗

この章の概要です。


入れ子クラス 🔗

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

入れ子クラスは、1つのスコープを形成します。そのため、入れ子クラスにアクセスするための完全な表記は、「Outer::Inner」のように、外側のクラス名と入れ子クラス名とを、スコープ解決演算子でつなげたものになります。

ただし、名前空間や staticメンバの場合と同様に、すでに Outer のスコープ内にいる場合は「Outer::」の部分を省略できます。

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

先ほど、入れ子クラスがスコープを形成すると書きましたが、その点では、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クラスに渡すようにすることも可能です。

入れ子クラスからは、外側のクラスの staticメンバへアクセスできます。そのメンバが「非公開」であっても問題ありません。たとえば、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]]

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

また、制約と呼ぶのがふさわしいかどうか分かりませんが、ローカルクラスのメンバ関数の定義は、ローカルクラスの定義の内側に書くしかありません。そのため、必然的にインライン展開の指定をしたことになります第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),
};

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


解答ページはこちら

参考リンク 🔗


更新履歴 🔗

 サイト全体で表記を統一(「静的メンバ」–>「staticメンバ」)

 コンパイラの対応状況について、対応している場合は明記しない方針にした。

 clang 3.7 (Xcode 7.3) を、Xcode 8.3.3 に置き換え。

 VisualC++ 2017 に対応。

 clang の対応バージョンを 3.7 に更新。

 新規作成。



前の章へ (第23章 テンプレートの特殊化)

次の章へ (第25章 フレンド)

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

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る