제 17 장: 내포 클래스

클래스 안에 또 클래스를 정의할 수 있다. 이런 클래스를 내포 클래스라고 부른다. 내포 클래스는 둘레 클래스와 개념적으로 관계가 밀접할 때 사용된다. 예를 들어 string 클래스에 string::iterator 유형을 사용할 수 있다. string 안에 저장된 모든 문자를 제공한다. string::iterator 유형은 iterator 객체로 정의하고 string 클래스 안에 내포 클래스로 정의할 수 있다.

내포 클래스는 둘레의 클래스 안 어디에든 배치할 수 있다. 공개 구역이나 보호 구역 또는 비밀 구역에 상관이 없다. 내포 클래스는 둘레 클래스의 멤버로 간주해도 된다. 클래스에 적용되는 규칙과 접근 방식이 내포 클래스에도 적용된다. 클래스가 다른 클래스의 공개 구역에 포함되면 둘레 클래스 밖에서 볼 수 있다. 보호 구역에 포함되면 그 아래의 파생 클래스에서 볼 수 있다. 비밀 구역에 내포되면 둘레 클래스의 멤버만 볼 수 있다.

둘레 클래스는 내포 클래스에 대하여 특별한 권한이 없다. 내포 클래스는 둘레 클래스가 자신의 멤버에 접근하는 것을 완전히 통제한다. 예를 들어 다음 클래스 정의를 연구해 보자:

    class Surround
    {
        public:
            class FirstWithin
            {
                int d_variable;

                public:
                    FirstWithin();
                    int var() const;
            };
        private:
            class SecondWithin
            {
                int d_variable;

                public:
                    SecondWithin();
                    int var() const;
            };
    };
    inline int Surround::FirstWithin::var() const
    {
        return d_variable;
    }
    inline int Surround::SecondWithin::var() const
    {
        return d_variable;
    }
내포 클래스 멤버의 접근 규칙은 다음과 같이 정의된다. 내포 클래스의 비밀 멤버에 둘레 클래스가 접근할 수 있도록 권한을 부여하려면 또는 둘레 클래스의 비밀 멤버에 내포 클래스가 접근할 수 있도록 권한을 부여하려면 클래스를 친구(friend)로 선언하면 된다 (17.3절).

내포 클래스는 둘레 클래스의 멤버로 간주할 수 있지만 내포 클래스의 멤버는 둘레 클래스의 멤버가 아니다. 그래서 Surround 클래스의 멤버는 FirstWithin::var에 직접 접근할 수 없다. 이것은 Surround 객체가 FirstWithin 객체도 아니고 SecondWithin 객체도 아닌 것을 고려하면 이해할 수 있다. 사실, 내포 클래스는 그저 유형의 이름(typenames)일 뿐이다. 내포 클래스의 객체가 자동으로 둘레 클래스 안에 존재하는 것은 아니라는 뜻이 아니다. 둘레 클래스의 멤버가 내포 클래스의 (비-정적) 멤버를 사용해야 한다면 둘레 클래스는 내포 클래스 실체를 정의해야 한다. 둘레 클래스의 멤버는 이를 이용하여 그의 멤버를 사용할 수 있다.

예를 들어 다음 클래스 정의에 둘레 클래스로 Outer가 있고 내포 클래스로 Inner가 있다. Outer 클래스는 caller 멤버 함수가 있다. caller 멤버 함수는 Outer 객체 안에 조립된 d_inner 객체를 사용하여 Inner::infunction 멤버를 호출한다.

    class Outer
    {
        public:
            void caller();

        private:
            class Inner
            {
                public:
                    void infunction();
            };
            Inner d_inner;      // Inner 클래스를 이미 알고 있어야 한다.
    };
    void Outer::caller()
    {
        d_inner.infunction();
    }
Inner::infunction 멤버는 Outer::caller 멤버의 인라인 정의의 일부로 호출할 수 있다. Inner 클래스의 정의가 아직 컴파일러에게 보이지 않지만 말이다. 반면에 컴파일러는 Inner 클래스의 데이터 멤버가 정의되기 전에 먼저 그 클래스의 정의를 미리 보았어야 한다.

17.1: 내포 클래스 멤버 정의하기

내포 클래스의 멤버 함수는 인라인 함수로 정의할 수 있다. 인라인 멤버 함수는 클래스 정의 바깥에 정의할 수 있다. Outer::caller 멤버 함수를 Outer 클래스의 밖에 정의하려면 그 함수의 이름을 (둘레 클래스 영역으로부터 시작하여 (Outer)) 완전히 자격을 갖추어 컴파일러에게 제공해야 한다. 인라인 함수와 인-클래스 함수도 그에 맞게 정의할 수 있다. 그러면 내포 클래스를 얼마든지 사용할 수 있다. 내포 클래스의 정의가 나중에 둘레 클래스 인터페이스에 나타날지라도 말이다.

(내포된) 멤버 함수가 인라인으로 선언되어 있으면 그 정의는 클래스 인터페이스보다 아래에 배치해야 한다. 내포된 정적 데이터 멤버도 클래스 밖에 정의하는 것이 보통이다. FirstWithin 클래스에 static size_t epoch 데이터 멤버가 있다면 다음과 같이 초기화할 수 있다.

    size_t Surround::FirstWithin::epoch = 1970;
둘레 클래스의 바깥 코드에 있는 공개 정적 멤버를 참조하려면 영역 결정 연산자가 여러 개 필요하다.
    void showEpoch()
    {
        cout << Surround::FirstWithin::epoch;
    }
Surround 클래스 안에서 FirstWithin:: 영역만 사용해야 한다. FirstWithin 클래스 안에서는 명시적으로 영역을 참조할 필요가 없다.

SecondWithin 클래스의 멤버는 어떤가? FirstWithin 클래스와 SecondWithin 클래스는 둘 다 Surround 안에 내포되어 있고 둘레 클래스의 멤버로 간주할 수 있다. 클래스의 멤버는 서로 직접 참조가 가능하므로 SecondWithin 클래스의 멤버는 FirstWithin 클래스의 공개 멤버를 참조할 수 있다. 결과적으로 SecondWithin 클래스의 멤버는 FirstWithinepoch 멤버에 FirstWithin::epoch와 같이 접근할 수 있다.

17.2: 내포 클래스 선언하기

내포 클래스는 둘레 클래스 안에 실제로 정의하기 전에 먼저 선언할 수 있다. 클래스에 내포 클래스가 여러 개 있고 그 내포 클래스 안에 다른 내포 클래스의 객체에 대한 포인터와 참조 매개변수나 반환 유형이 있다면 전방 선언이 필요하다.

예를 들어 다음 Outer 클래스 안에 Inner1Inner2 두 개의 내포 클래스가 있다. Inner1 클래스 안에 Inner2 객체를 가리키는 포인터가 있고 Inner2 클래스에 Inner1의 객체를 가리키는 포인터가 있다. 교차 참조는 전방 선언이 요구된다. 전방 선언은 정의에 지정한 접근 권한과 동일하게 접근 권한을 주어야 한다. 다음 예제에서 Inner2 전방 선언은 비밀 구역에 주어졌다. 마찬가지로 그의 정의도 Outer 클래스의 비밀 인터페이스에 포함된다.

    class Outer
    {
        private:
            class Inner2;       // 전방 선언

            class Inner1
            {
                Inner2 *pi2;    // Inner2의 실체를 가리킨다.
            };
            class Inner2
            {
                Inner1 *pi1;    // Inner1의 실체를 가리킨다.
            };
    };

17.3: 내포 클래스 안에 있는 비밀 멤버에 접근하기

다른 내포 클래스의 비밀 멤버에 접근할 권한을 내포 클래스에 부여하려면 또는 내포 클래스의 비밀 멤버에 접근할 권한을 둘레 클래스에 부여하려면 friend 키워드를 사용해야 한다.

친구 선언을 하지 않아도 내포 클래스는 둘레 클래스의 비밀 멤버에 접근할 권한이 있음을 눈여겨보라. 어쨌거나 내포 클래스는 둘레 클래스가 정의한 한 유형이며 내포 클래스의 실체는 둘레 클래스의 멤버이다. 그러므로 둘레 클래스의 모든 멤버에 접근할 수 있다. 다음은 이 원리를 보여주는 예이다. 예제는 컴파일되지 않는다. Extern 클래스의 멤버가 Outer의 비공개 멤버에 접근하지 못하기 때문이다. 그러나 Outer::Inner의 멤버는 Outer의 비공개 멤버에 접근할 수 있다.

    class Outer
    {
        int d_value;
        static int s_value;

        public:
            Outer()
            :
                d_value(12)
            {}
            class Inner
            {
                public:
                    Inner()
                    {
                        cout << "Outer's static value: " << s_value << '\n';
                    }
                    Inner(Outer &outer)
                    {
                        cout << "Outer's value: " << outer.d_value << '\n';
                    }
            };
    };
    class Extern            // 컴파일되지 않는다!
    {
        public:
            Extern(Outer &outer)
            {
                cout << "Outer's value: " << outer.d_value << '\n';
            }

            Extern()
            {
                cout << "Outer's static value: " << Outer::s_value << '\n';
            }
    };

    int Outer::s_value = 123;
    int main()
    {
        Outer outer;
        Outer::Inner in1;
        Outer::Inner in2(outer);
    }

이제 Surround 클래스에 FirstWithinSecondWithin 두 개의 클래스가 있다고 생각해 보자. 세 클래스는 각각 정적 int s_variable 멤버가 있다.

    class Surround
    {
        static int s_variable;
        public:
            class FirstWithin
            {
                static int s_variable;
                public:
                    int value();
            };
            int value();
        private:
            class SecondWithin
            {
                static int s_variable;
                public:
                    int value();
            };
    };
Surround 클래스가 FirstWithinSecondWithin의 비밀 멤버에 접근해야 한다면 이 두 개의 클래스는 Surround를 친구로 선언해야 한다. 그러면 Surround::value 함수는 내포 클래스의 비밀 멤버에 접근할 수 있다. 예를 들어 (내포 클래스에 선언된 friend 키워드를 눈여겨보라):
    class Surround
    {
        static int s_variable;
        public:
            class FirstWithin
            {
                friend class Surround;
                static int s_variable;
                public:
                    int value();
            };
            int value();
        private:
            class SecondWithin
            {
                friend class Surround;
                static int s_variable;
                public:
                    int value();
            };
    };
    inline int Surround::FirstWithin::value()
    {
        FirstWithin::s_variable = SecondWithin::s_variable;
        return (s_variable);
    }
친구로 간주될 객체의 정의 다음에 친구를 선언한다. 그래서 클래스를 정의한 다음에 친구로 선언할 수 있다. 이 경우 인-클래스 코드는 이미 앞으로 나올 클래스가 친구로 선언할 것이라는 사실을 이용할 수 있다. 예를 들면 Surround::FirstWithin::value 함수의 인-클래스 구현을 살펴보자. 필수 friend 선언을 value 함수의 구현 다음에 삽입해도 된다.
    class Surround
    {
        public:
            class FirstWithin
            {
                static int s_variable;
                public:
                    int value();
                    {
                        FirstWithin::s_variable = SecondWithin::s_variable;
                        return s_variable;
                    }
                friend class Surround;
            };
        private:
            class SecondWithin
            {
                friend class Surround;
                static int s_variable;
            };
    };

안쪽 클래스와 바깥쪽 클래스에서 이름이 동일한 (`s_variable') 멤버는 아래에 기술하듯이 적절한 영역 결정 표현식을 사용하여 접근할 수 있다.

    class Surround
    {
        static int s_variable;
        public:
            class FirstWithin
            {
                friend class Surround;
                static int s_variable;  // 이름이 같다.
                public:
                    int value();
            };
            int value();

        private:
            class SecondWithin
            {
                friend class Surround;
                static int s_variable;  // 이름이 같다.
                public:
                    int value();
            };
            static void classMember();
    };
    inline int Surround::value()
    {                                   // 영역 결정 표현식
        FirstWithin::s_variable = SecondWithin::s_variable;
        return s_variable;
    }
    inline int Surround::FirstWithin::value()
    {
        Surround::s_variable = 4;       // 영역 결정 표현식
        Surround::classMember();
        return s_variable;
    }
    inline int Surround::SecondWithin::value()
    {
        Surround::s_variable = 40;      // 영역 결정 표현식
        return s_variable;
    }

내포 클래스는 자동으로 서로의 친구가 되지는 않는다. friend 선언을 해야 비로서 서로의 비밀 멤버에 접근할 수 있다.

FirstWithinSecondWithin의 비밀 멤버에 접근하려면 friend 선언이 SecondWithin에 있어야 한다.

마찬가지로 FirstWithin 클래스는 그냥 friend class SecondWithin를 사용하여 SecondWithin 클래스가 FirstWithin 클래스의 비밀 멤버에 접근하는 것을 허용한다. 컴파일러가 아직 SecondWithin를 못 보았지만 친구 선언도 전방 선언으로 간주된다.

그러나 SecondWithinFirstWithin의 비밀 멤버에 접근할 수 있도록 권한을 부여하려면 FirstWithin은 단순히 friend class SecondWithin을 사용하는 정도로는 부족하다. SecondWithin의 정의가 아직 알려져 있지 않기 때문이다.

이제 SecondWithin의 전방 선언이 필요하다. 이 전방 선언은 Surround 클래스가 제공해야 한다. FirstWithin 클래스가 제공하는 것이 아니다. FirstWithin 클래스 자체에서 `class SecondWithin;'와 같이 전방 선언을 지정하는 것은 의미가 없다. 이렇게 하면 외부의 (전역) SecondWithin 클래스를 참조하기 때문이다.

SecondWithin의 전방 선언은 FirstWithin안에서 `class Surround::SecondWithin;'와 같이 지정할 수 없음을 주목하자. 이 시도는 다음과 같은 에러 메시지를 만들어 낼 것이다.

`Surround' does not have a nested type named `SecondWithin'
`Surround'에 이름이 `SecondWithin'인 유형이 내포되어 있지 않습니다

내포 클래스 안에서 SecondWithin에 대하여 전방 선언을 하는 대신에 FirstWithin 클래스가 정의되기 전에 먼저 SecondWithin 클래스를 Surround 클래스가 선언해야 한다. 이런 식으로 SecondWithin의 친구 선언을 하는 것은 FirstWithin 안에서 허용된다.

내포된 SecondWithin 클래스 말고도 바깥 수준에 SecondWithin 클래스가 존재한다고 가정하자. 그 클래스를 FirstWithin 클래스의 친구로 선언하려면 class FirstWithin 안에 friend ::SecondWithin를 선언하라. 그 경우 바깥 수준에서 FirstWithin의 클래스 선언을 먼저 해야 컴파일러가 friend::SecondWithin 선언을 만날 수 있다.

다음은 관련된 모든 클래스의 모든 비밀 멤버에 모든 클래스가 완전히 접근하는 예이다.

    class SecondWithin;
    class Surround
    {
        // class SecondWithin;      필수가 아니다 (에러도 아님): 
        //                          친구 선언 (아래 참고)도
        //                          전방 선언이다.
      static int s_variable;
        public:
            class FirstWithin
            {
                friend class Surround;
                friend class SecondWithin;
                friend class ::SecondWithin;

                static int s_variable;
                public:
                    int value();
            };
            int value();            // 구현은 위에 있음.
        private:
            class SecondWithin
            {
                friend class Surround;
                friend class FirstWithin;
                static int s_variable;
                public:
                    int value();
            };
    };
    inline int Surround::FirstWithin::value()
    {
        Surround::s_variable = SecondWithin::s_variable;
        return s_variable;
    }
    inline int Surround::SecondWithin::value()
    {
        Surround::s_variable = FirstWithin::s_variable;
        return s_variable;
    }

17.4: 내포된 열거체

열거체도 클래스 안에 내포시킬 수 있다. 열거체를 내포시키는 것은 열거체와 클래스 사이의 밀접한 관련성을 보여주는 좋은 방법이다. 내포된 열거체는 다른 클래스 멤버와 가시 규칙이 똑 같다. 클래스의 비밀 구역이나 보호 구역 또는 공개 구역에 정의될 수 있으며 파생 클래스에게 상속이 가능하다. ios 클래스에서 ios::begios::cur 같은 값들을 본 바가 있다. 현재의 Gnu C++ 구현에서 이 값들은 seek_dir 열거체의 값으로 정의되어 있다.
    class ios: public _ios_fields
    {
        public:
            enum seek_dir
            {
                beg,
                cur,
                end
            };
    };
예를 들어 DataStructure 클래스는 앞뒤로 순회가 가능한 데이터 구조를 나타낸다고 간주하자. 이 클래스는 Traversal 열거체를 정의하여 FORWARD 값과 BACKWARD 값을 가질 수 있다. 게다가 setTraversal 멤버 함수를 정의하여 Traversal 유형의 인자를 요구할 수 있다. 대략 다음과 같이 정의할 수 있다.
    class DataStructure
    {
        public:
            enum Traversal
            {
                FORWARD,
                BACKWARD
            };
            setTraversal(Traversal mode);
        private:
            Traversal
                d_mode;
    };
DataStructure 클래스 안에서 Traversal 열거체의 값을 직접 사용할 수 있다. 예를 들어:
    void DataStructure::setTraversal(Traversal mode)
    {
        d_mode = mode;
        switch (d_mode)
        {
            FORWARD:
                // ... 일을 한다.
            break;

            BACKWARD:
                // ... 다른 일을 한다.
            break;
        }
    }
DataStructure 클래스의 밖에서 열거체 유형의 이름은 열거체의 값을 참조하는데 사용되지 않는다. 여기에서는 클래스 이름이면 충분하다. 열거체의 값이 요구되는 경우에만 열거체 유형의 이름이 필요하다. 다음 코드에 이를 보여준다.
    void fun()
    {
        DataStructure::Traversal                // 열거 유형이름 필요
            localMode = DataStructure::FORWARD; // 열거 유형이름 불필요

        DataStructure ds;
                                                // 열거 유형이름 불필요
        ds.setTraversal(DataStructure::BACKWARD);
    }
위의 예제에서 DataStructure 클래스에 정의된 열거체의 값으로 DataStructure::FORWARD 상수를 지정했다. DataStructure::FORWARD 대신에 ds.FORWARD 구조도 허용된다. 이 구문적 방종은 보기에 흉하다고 필자는 본다. FORWARD는 클래스 수준에 정의된 심볼 값이다. ds의 멤버가 아니다. 멤버 선택 연산자를 사용했기 때문에 오해를 부른다.

DataStructure 클래스에 Nested 내포 클래스가 정의되어 있고 이어서 Traversal 열거체가 정의되어 있을 경우에만 두 클래스의 영역이 요구된다. 그 경우 후자의 예제는 다음과 같이 코딩했어야 한다.

    void fun()
    {
        DataStructure::Nested::Traversal
            localMode = DataStructure::Nested::FORWARD;

        DataStructure ds;

        ds.setTraversal(DataStructure::Nested::BACKWARD);
    }
여기에서 DataStructure::Nested::Traversal localMode = ds.Nested::FORWARD 구조를 사용할 수도 있었다. 필자는 그것을 피하고 싶었다. FORWARDds의 멤버가 아니라 DataStructure에 정의된 심볼이기 때문이다.

17.4.1: 빈 열거체

열거체는 심볼 값을 정의한다. 그렇지만 꼭 그래야만 하는 것은 아니다. 14.6.1 항에 std::bad_cast 유형을 소개했다. dynamic_cast<> 연산자는 바탕 클래스 실체의 참조를 파생 클래스 참조로 변경할 수 없을 때 bad_cast를 던진다. bad_cast는 값에 상관없이 유형으로 나포할 수 있다.

유형은 연관 값 없이 정의할 수 있다. 빈 열거체를 정의할 수 있다. 값이 정의되지 않은 enum이다. 빈 열거체의 유형 이름은 그 이후 예를 들어 catch 절 같은 곳에서 합법적인 유형으로 사용된다.

예제는 빈 열거체를 어떻게 정의하는지 보여준다. 반드시는 아니지만 class 안에 자주 정의된다. 그리고 어떻게 예외로 던지고 잡을 수 있는지 보여준다.

    #include <iostream>

    enum EmptyEnum
    {};

    int main()
    try
    {
        throw EmptyEnum();
    }
    catch (EmptyEnum)
    {
        std::cout << "Caught empty enum\n";
    }

17.5: 가상 생성자 심층 연구

14.12절에 가상 생성자 표기법을 소개했다. 그 절에서 Base 클래스를 추상 바탕 클래스로 정의했다. 컨테이너에 담긴 Base 클래스 포인터를 마치 벡터처럼 관리하도록 Clonable 클래스를 정의했다.

Base 클래스는 구현이 거의 필요 없는 초소형 클래스이므로 Clonable에 내포 클래스로 정의하기에 딱 좋다. 이렇게 하면 ClonableBase 사이의 밀접한 관계가 명확하게 드러난다. BaseClonable 아래에 내포시켜 다음을

    class Derived: public Base
다음과 같이 바꾼다.
    class Derived: public Clonable::Base
Base를 내포 클래스로 정의하고 Base가 아니라 Clonable::Base로부터 파생시키는 것 말고는 (즉, Base 멤버에 적절하게 Clonable:: 이름공간을 앞에다 붙여서 완전히 자격을 갖춘 이름을 완성하는 것 말고는) 더 이상 변경할 필요가 없다. 다음은 앞서 보여준 프로그램에서 변경한 부분이다 (14.12절). 이제 Clonable에 내포된 Base를 사용한다.
// 복제 가능한 내포된 Base, 인라인 멤버 포함:
    class Clonable
    {
        public:
            class Base;
        private:
            Base *d_bp;
        public:
            class Base
            {
                public:
                    virtual ~Base();
                    Base *clone() const;
                private:
                    virtual Base *newCopy() const = 0;
            };
            Clonable();
            explicit Clonable(Base *base);
            ~Clonable();
            Clonable(Clonable const &other);
            Clonable(Clonable &&tmp);
            Clonable &operator=(Clonable const &other);
            Clonable &operator=(Clonable &&tmp);

            Base &base() const;
    };
    inline Clonable::Base *Clonable::Base::clone() const
    {
        return newCopy();
    }
    inline Clonable::Base &Clonable::base() const
    {
        return *d_bp;
    }

// 파생 멤버와 그의 인라인 멤버:
    class Derived1: public Clonable::Base
    {
        public:
            ~Derived1();
        private:
            virtual Clonable::Base *newCopy() const;
    };
    inline Clonable::Base *Derived1::newCopy() const
    {
        return new Derived1(*this);
    }

// 인라인으로 구현이 안된 멤버들:
    Clonable::Base::~Base()
    {}