제 4 장: 이름공간

4.1: 이름공간

대화식 수학 프로그램을 개발하고 싶다고 해 보자. 이 프로그램에서 cos, sin, tan 등등의 함수는 각도로 인자를 받도록 되어 있다. 그런데 함수 이름 cos는 이미 사용 중이고 호도를 인자로 받는다.

이런 문제는 또다른 이름을 정의하면 해결된다. 예를 들어 함수 이름을 cosDegrees로 정의하면 된다. 그러나 C++이름공간을 통하여 또다른 해결책을 제시한다. 이름공간은 식별자가 정의되어 있는 코드 영역으로 간주할 수 있다. 이름공간에 정의된 식별자들은 이미 다른 곳에 (이름공간 밖에) 정의된 이름과 충돌하지 않는다. 그래서 (각도를 자신의 인자로 기대하는) cos 함수를 Degrees 이름공간에 정의할 수 있다. cosDegrees 안에서 호출하면 각도를 기대하는 cos 함수를 호출할 수 있다. 호도를 기대하는 표준 cos 함수가 호출되지 않는다.

ANSI/ISO 표준은 최신 컴파일러에서 상당한 수준까지 구현되었으므로 이제 이름공간이 더 엄격하게 사용된다. 이 때문에 class 헤더를 설정하는 효과가 있다. 지금은 자세하게 다루지 못하지만 7.11.1항에 이름공간으로부터 객체들을 사용하는 헤더 파일을 구성하는 법을 다룬다.

이름공간은 클래스에 없는 부가 기능이 있다.

4.1.1: 이름공간 정의하기

이름공간은 다음 구문에 맞게 정의된다.
    namespace identifier
    {
        // 선언된 또는 정의된 개체
        // (선언 구역)
    }
이름공간을 정의할 때 사용된 식별자는 표준 C++ 식별자이다.

위의 예제 코드에 보여주듯이 선언 구역 안에 함수나 변수 또 구조체나 클래스 그리고 심지어 (내포) 이름공간도 정의하거나 선언할 수 있다. 이름공간은 함수 몸체 안에 정의할 수 없다. 그렇지만 여러 namespace 선언을 사용하여 이름공간을 정의하는 것은 가능하다. 이름공간은 `열려 있다'. 그 의미는 CppAnnotations 이름공간을 파일 file1.ccfile2.cc에 정의할 수 있다는 뜻이다. file1.cc 파일과 file2.cc 파일의 CppAnnotations 이름공간 안에 정의된 개체들은 하나의 CppAnnotations 이름공간 안에 통합된다. 예를 들어:

    // in file1.cc
    namespace CppAnnotations
    {
        double cos(double argInDegrees)
        {
            ...
        }
    }

    // in file2.cc
    namespace CppAnnotations
    {
        double sin(double argInDegrees)
        {
            ...
        }
    }
이제 sin 함수와 cos 함수 모두 같은 CppAnnotations 이름공간에 정의된다.

이름공간 개체는 이름공간 밖에 정의할 수 있다. 이 주제는 4.1.4.1목에 다룬다.

4.1.1.1: 이름공간에 개체 선언하기

이름공간 안에 개체를 정의하는 대신에 선언할 수도 있다. 이렇게 하면 모든 선언을 하나의 헤더 파일에 두고 거기에 소스를 포함시켜서 이름공간에 정의된 개체들을 사용할 수 있다. 그런 헤더 파일은 다음과 같은 내용을 담을 수 있다.
    namespace CppAnnotations
    {
        double cos(double degrees);
        double sin(double degrees);
    }

4.1.1.2: 닫힌 이름공간

이름공간은 이름없이 정의할 수 있다. 이름없는 이름공간에 개체를 정의하면 그 소스 파일에 보이지 않는다.

익명 이름공간에 정의된 객체들은 비교하자면 Cstatic 함수나 변수와 같다. C++에서도 여전히 static 키워드를 사용할 수 있지만 주 사용법은 class 정의에 있다 (7장). C에서 정적 변수나 함수가 사용되어야 하는 상황에 C++에서는 익명 이름공간을 사용해야 한다.

익명 이름공간은 닫힌 이름공간이다. 즉, 익명 이름공간에 개체를 추가하더라도 다른 소스 파일의 익명 공간에 보이지 않는다.

4.1.2: 개체 참조하기

이름공간과 개체가 주어지면 영역 지정 연산자를 사용하여 그 개체를 참조할 수 있다. 예를 들어 CppAnnotations 이름공간에 정의된 cos() 함수는 다음과 같이 사용할 수 있다.
    // CppAnnotations 이름공간이 다음 헤더 파일에
    // 정의되어 있다고 간주:
    #include <cppannotations>

    int main()
    {
        cout << "The cosine of 60 degrees is: " <<
                CppAnnotations::cos(60) << '\n';
    }
CppAnnotations 이름공간에 있는 cos() 함수를 참조하기에 이 방법은 약간 귀찮다. 특히 함수를 자주 사용해야 할 경우는 더 그렇다. 그러면 선언한 다음에 간략 형태를 사용하면 된다.
    using CppAnnotations::cos;  // 주의: 함수 원형 없음,
                                // 그냥 개체 이름만
                                // 있으면 됨.
cos를 호출하면 CppAnnotations 이름공간에 정의된 cos 함수가 호출된다. 이것은 호도를 받는 표준 cos 함수가 더 이상 자동으로 호출되지 않는다는 뜻이다. 표준 cos 함수를 호출하려면 평범하게 영역 지정 연산자를 사용해야 한다.
    int main()
    {
        using CppAnnotations::cos;
        ...
        cout << cos(60)         // CppAnnotations::cos()를 호출
            << ::cos(1.5)       // 표준 cos() 함수를 호출
            << '\n';
    }
using 선언은 영역을 제한한다. 블록 안에 사용할 수 있다. using 선언은 using 선언에 사용된 것과 이름이 같은 개체가 정의되는 것을 방지한다. 어떤 이름공간에 있는 value 변수에 대하여 using 선언을 지정하는 것은 불가능하다. using 선언이 포함된 블록 안에 이름이 동일한 객체를 정의하거나 선언하는 것은 불가능하다. 예를 들어:
    int main()
    {
        using CppAnnotations::value;
        ...
        cout << value << '\n';  // CppAnnotations::value 사용
        int value;              // 에러: 이미 선언된 값.
    }

4.1.2.1: `using' 지시어

using 선언에 대한 일반적인 지시어는 using 지시어이다.
    using namespace CppAnnotations;
이 지시어 다음부터 CppAnnotations 이름공간 안에 정의된 모든 개체들은 마치 using 선언이 된 것처럼 사용된다.

(그 이름공간이 미리 선언되어 있거나 정의되어 있다는 가정하에) using 지시어가 한 이름공간 안에 있는 모든 이름을 반입하는 빠른 방법이기는 하지만 약간 지저분한 방법이기도 하다. 특정 코드 블록 안에 실제로 어떤 개체가 사용중인지 좀 명확하지 않기 때문이다.

예를 들어 만약 cos 함수가 CppAnnotations 이름공간에 정의되어 있다면 cos 함수가 호출될 때 CppAnnotations::cos가 사용될 것이다. 그러나 cosCppAnnotations 이름공간 안에 정의되어 있지 않다면 표준 cos 함수가 사용될 것이다. 어떤 개체가 실제로 사용될 지에 관하여 using 지시어는 using 선언만큼 명확하게 문서화가 되어 있지 않다. 그러므로 using 지시어를 사용할 때 조심하라.

4.1.2.2: `쾨닉 검색(Koenig lookup)'

쾨닉 검색을 `쾨닉 원리(Koenig principle)'라고 부른다면 새로운 추리 소설(Ludlum novel)의 제목으로 그럴싸 해 보이겠지만 사실은 C++의 한 가지 기술을 가리킨다.

`쾨닉 검색'이 가리키는 것은 다음과 같다. 이름공간을 지정하지 않고 함수를 호출하면 인자 유형의 이름공간을 따라 함수의 이름공간을 결정한다. 인자 유형이 정의된 이름공간에 그런 함수가 있다면 그 함수가 사용된다. 이런 절차를 `쾨닉 검색(Koenig lookup)'이라고 부른다.

예를 들어 다음 예제를 생각해 보자. FBB::fun(FBB::Value v) 함수가 FBB 이름공간에 정의되어 있다. 명시적으로 이름공간을 언급하지 않아도 호출할 수 있다.

    #include <iostream>

    namespace FBB
    {
        enum Value        // FBB::Value 정의
        {
            FIRST
        };

        void fun(Value x)
        {
            std::cout << "fun called for " << x << '\n';
        }
    }

    int main()
    {
        fun(FBB::FIRST);    // 쾨닉 검색: 이름공간이
                            // fun()에 지정되어 있지 않으므로
    }
    /*
        출력:
    fun called for 0
    */

컴파일러는 이름공간을 똑똑하게 처리한다. namespace FBB 안의 Valuetypedef int Value로 정의되어 있다면 FBB::Valueint로 인지될 것이고 그리하여 쾨닉 찾기는 실패한다.

또다른 예제로 다음 프로그램을 생각해 보자. 두 개의 이름공간이 관련되어 있다. 이름공간마다 자신만의 fun 함수가 정의되어 있다. 애매모호한 것은 전혀 없다. 인자가 이름공간을 정의하고 있기 때문에 FBB::fun 멤버 함수가 호출된다.

    #include <iostream>

    namespace FBB
    {
        enum Value        // FBB::Value를 정의한다.
        {
            FIRST
        };

        void fun(Value x)
        {
            std::cout << "FBB::fun() called for " << x << '\n';
        }
    }

    namespace ES
    {
        void fun(FBB::Value x)
        {
            std::cout << "ES::fun() called for " << x << '\n';
        }
    }

    int main()
    {
        fun(FBB::FIRST);    // 애매 모호함 없음: 인자가
                            // 이름공간을 결정한다.
    }
    /*
        출력:
    FBB::fun() called for 0
    */

다음은 모호성이 있는 예제이다. fun 함수는 인자가 두 개이다. 각자의 이름공간으로부터 온다. 이 모호성은 프로그래머가 해결해야 한다.

    #include <iostream>

    namespace ES
    {
        enum Value        // ES::Value를 정의
        {
            FIRST
        };
    }

    namespace FBB
    {
        enum Value        // FBB::Value를 정의
        {
            FIRST
        };

        void fun(Value x, ES::Value y)
        {
            std::cout << "FBB::fun() called\n";
        }
    }

    namespace ES
    {
        void fun(FBB::Value x, Value y)
        {
            std::cout << "ES::fun() called\n";
        }
    }

    int main()
    {
        //  fun(FBB::FIRST, ES::FIRST); 모호함: 명시적으로
        //                              이름공간을 지정해
        //                              해결해야 함
        ES::fun(FBB::FIRST, ES::FIRST);
    }
    /*
        출력:
    ES::fun() called
    */

이름공간의 흥미로운 점은 한 이름공간에 정의된 것은 다른 이름공간에 정의된 코드를 깰 수도 있다는 것이다. 이름공간은 서로 영향을 미칠 수 있으며 그 기묘함을 인지하지 못하면 서로의 등에 총을 겨눌 수 있다는 사실을 보여준다. 다음 예제를 살펴보자:

    namespace FBB
    {
        struct Value
        {};

        void fun(int x);
        void gun(Value x);
    }

    namespace ES
    {
        void fun(int x)
        {
            fun(x);
        }
        void gun(FBB::Value x)
        {
            gun(x);
        }
    }

무슨 일이 일어나든 프로그래머는 ES::fun 함수에서 아무 것도 하지 않는 편이 좋다. 무한 재귀를 초래하기 때문이다. 그렇지만 그것이 요점은 아니다. 진짜 문제는 프로그래머에게 ES::fun 함수를 호출할 기회조차 없다는 것이다. 컴파일에 실패하기 때문이다.

gun에는 컴파일이 실패하지만 fun은 성공한다. 그러나 왜 그런가? 왜 ES::fun은 문제없이 컴파일에 성공하는 반면에 ES::gun은 실패하는가? ES::fun에서 fun(x)이 호출된다. x의 유형이 이름공간 안에 정의되어 있지 않으므로 쾨닉 검색이 적용되지 않는다. 그래서 fun은 자기 자신을 무한정 호출한다.

ES::gun 멤버는 인자가 FBB 이름공간에 정의되어 있다. 결과적으로 FBB::gun 함수는 호출 가능한 대상이다. 그러나 ES::gun 자체도 역시 호출 가능하다. ES::gun의 원형이 정확하게 gun(x) 호출에 부합하기 때문이다.

이제 FBB::gun이 아직 선언되어 있지 않은 상황을 생각해보자. 그러면 물론 모호함은 없다. ES 이름공간에 책임을 진 프로그래머는 흐뭇하게 바라본다. FBB 이름공간을 관리하는 프로그래머가 gun(Value x)FBB 이름공간에 배치하면 좋겠다는 결정을 내리자마자 갑자기 ES 공간에 있는 코드가 깨지기 시작한다. 완전히 다른 이름공간(FBB)에 뭔가 추가되었다는 이유만으로 말이다. 이름공간은 서로 완벽하게 독립적인 것은 아니라는 사실은 명확하다. 그러므로 위와 같은 경우를 세심하게 살펴야 한다. 나중에 이 문제에 관해 더 자세히 다루어 보겠다 (제 11장).

쾨닉 검색은 이름공간의 문맥에서만 사용된다. 함수가 이름공간 밖에 정의되어 있는데, 매개변수는 이름공간 안에 정의된 유형이고, 그 이름공간이 동일한 서명의 함수를 정의하고 있다면, 그 함수를 호출할 때 컴파일러는 모호하다고 보고한다. 다음은 예제이다. 위에 언급된 FBB 이름공간을 사용할 수 있다고 간주한다:

    void gun(FBB::Value x);

    int main(int argc, char **argv)
    {
        gun(FBB::Value{});          // 모호함: FBB::gun 그리고 ::gun 모두
                                    // 호출이 가능하다.
    }

4.1.3: 표준 이름공간

std 이름공간은 C++에 예약되어 있다. 표준 이름공간에 실행시간 소프트웨어에 사용가능한 개체들이 많이 정의되어 있다 (예를 들어, cout, cin, cerr); 표준 템플릿 라이브러리에 정의된 템플릿 (제 18장) 그리고 총칭 알고리즘 (제 19장) 등등이 std 이름공간에 정의되어 있다.

이전 절의 연구에 의하면 std 이름공간에 있는 개체들을 가리킬 때 using 선언을 사용할 수 있다. 예를 들어 std::cout 스트림을 사용하려면 코드는 이 객체를 다음과 같이 선언하면 된다.

    #include <iostream>
    using std::cout;
그렇지만 가끔 std 이름공간에 정의된 식별자를 별 생각없이 모조리 받아 들일 수도 있다. 프로그래머가 using 지시어를 사용하는 것을 자주 보는데 이름공간을 생략해도 그 이름공간에 정의된 모든 개체들을 가리킬 수 있기 때문이다. using 선언을 지정하는 대신에 다음 using 지시어도 다음과 같은 구조로 자주 볼 수 있다.
    #include <iostream>
    using namespace std;
using 선언 대신에 using 지시어를 사용해야 할까? 지켜야 할 제일 규칙은 using 선언을 사용하기로 결정할 수 있다는 것이다. 리스트가 터무니없이 길 경우까지 그렇다. 이 시점을 넘어서면 using 지시어를 고려할 수 있다.

using 지시어와 선언에는 두 가지 제한이 적용된다.

4.1.3.1: std::placeholders 이름공간

이 목은 앞으로 다룰 참조를 상당히 많이 담고 있다. 그냥 위치보유자(placeholders) 이름공간만 소개한다. 이 이름공간은 표준 이름공간 안에 들어 있다. 그러므로 이 목은 건너 뛰어도 흐름을 깨지 않는다.

std::placeholders 이름공간을 사용하기 전에 먼저 <functional> 헤더를 포함해야 한다.

이 책을 더 읽어 가다 보면 함수객체를 만날 것이다 (11.10절). 함수객체란 함수처럼 사용할 수 있는 `객체'이다. (펑크터(functors)라고도 부르는) 함수객체는 표준 템플릿 라이브러리에서 광범위하게 사용된다 (제 18장 STL 참고). STL이 제공하는 함수 중에는 함수 어댑터를 돌려주는 (bind라는) 함수가 있다(18.1.4.1목). 그 안에서 함수가 호출되는데 이 함수는 인자를 이미 받았을 수도 안 받았을 수도 있다. 인자가 지정되어 있지 않다면 위치보유자를 사용해야 한다. bind가 돌려준 해당 함수객체를 호출할 때 거기에다 실제 인자를 지정해야 한다.

그런 위치보유자는 이미 _1, _2, _3, 등등으로 이름으로 std::placeholders 이름공간에 정의되어 있다. 그의 다양한 사용법은 18.1.4.1목에서 볼 수 있다.

4.1.4: 이름공간 내포와 별칭

이름공간은 내포할 수 있다. 다음은 한 예이다.
    namespace CppAnnotations
    {
        int value;
        namespace Virtual
        {
            void *pointer;
        }
    }
value 변수는 CppAnnotations 이름공간 안에 정의된다. CppAnnotations 이름공간 안에 또다른 이름공간이 내포된다 (Virtual). 두 번째 이름공간 안에 pointer 변수가 정의된다. 이 변수를 참조하려면 다음 방법을 사용할 수 있다.

using namespace 지시어 다음부터 모든 개체는 그 이름공간에 해당되므로 더 이상 이름공간 없이 사용할 수 있다. 내포된 이름공간을 가리키기 위해 using namespace 지시어를 하나만 사용하면 그 내포된 이름공간의 모든 개체들은 더 이상 이름공간이 없어도 사용이 가능하다. 그렇지만 그 위 이름공간에 정의된 개체들은 여전히 얕은 이름공간이 필요하다. 구체적으로 using namespace 지시어나 using 선언을 한 후에만 이름공간 자격 부여를 생략할 수 있다.

완전히 자격을 갖춘 이름이 더 좋겠지만 다음과 같이 이름이 너무 길면

    CppAnnotations::Virtual::pointer
이름공간 별칭을 사용할 수 있다.
    namespace CV = CppAnnotations::Virtual;
이렇게 하면 CV를 전체 이름에 대한 별칭으로 사용한다. pointer 변수는 이제 다음과 같이 접근해도 된다.
    CV::pointer = 0;
이름공간 별칭은 using namespace 지시어나 using 선언에도 사용할 수 있다.
    namespace CV = CppAnnotations::Virtual;
    using namespace CV;

내포된 이름공간 정의 (C++17)

C++17 표준부터 내포된 이름공간을 영역 결정 연산자를 사용하여 직접적으로 참조할 수 있다. 예를 들어,

    namespace Outer::Middle::Inner
    { 
        // 여기에 정의/선언된 개체는 Inner 이름공간에 정의되며
        // Inner 이름공간은 Middle 이름공간 안에 정의된다.
        // 이어서 Middle 이름공간은 Outer 이름공간 안에 정의된다.
    }

4.1.4.1: 이름공간 밖에 개체 정의하기

이름공간의 멤버들을 이름공간 안에 정의해야 하는 것은 아니다. 그러나 개체를 이름공간 밖에 정의하기 전에 먼저 이름공간 안에 선언해야 한다.

이름공간 밖에 개체를 정의하려면 이름 앞에 이름공간을 붙여서 완전히 자격을 갖추어야 한다. 전역 수준에 정의를 하거나 아니면 이름공간에 내포될 경우에 그 사이 수준에 정의할 수 있다. 이렇게 하면 이름공간 A 안의 이름공간 A::B에 속하는 개체를 정의할 수 있다.

int INT8[8] 유형이 CppAnnotations::Virtual 이름공간에 정의되어 있다고 가정해 보자. 또 squares 함수를 이름공간 CppAnnotations::Virtual에 정의하고 싶다고 해보자. CppAnnotations::Virtual::INT8를 포인터로 돌려준다.

필수조건은 CppAnnotations::Virtual 이름공간 안에 정의했으므로 우리의 함수는 다음과 같이 정의할 수 있다 (메모리 배당 연산자 new[]에 관한 연구는 제 9장을 참고하라):

    namespace CppAnnotations
    {
        namespace Virtual
        {
            void *pointer;

            typedef int INT8[8];

            INT8 *squares()
            {
                INT8 *ip = new INT8[1];

                for (size_t idx = 0; idx != sizeof(INT8) / sizeof(int); ++idx)
                    (*ip)[idx] = (idx + 1) * (idx + 1);

                return ip;
            }
        }
    }
squares 함수는 INT8 벡터의 배열을 정의하고 그 벡터를 8까지 자연수의 제곱으로 초기화한 후에 그 주소를 돌려준다.

이제 squares 함수는 CppAnnotations::Virtual 이름공간 밖에 정의할 수 있다.

    namespace CppAnnotations
    {
        namespace Virtual
        {
            void *pointer;

            typedef int INT8[8];

            INT8 *squares();
        }
    }

    CppAnnotations::Virtual::INT8 *CppAnnotations::Virtual::squares()
    {
        INT8 *ip = new INT8[1];

        for (size_t idx = 0; idx != sizeof(INT8) / sizeof(int); ++idx)
            (*ip)[idx] = (idx + 1) * (idx + 1);

        return ip;
    }
위의 코드에서 다음 사실을 눈여겨보자:

마지막으로, 함수를 CppAnnotations 이름공간 안에 정의해도 됨을 눈여겨보자. 그 경우 squares() 함수를 정의하고 반환 유형을 지정할 때 Virtual 이름공간이 필요할 것이다. 반면에 함수의 내부는 그대로이다.

    namespace CppAnnotations
    {
        namespace Virtual
        {
            void *pointer;

            typedef int INT8[8];

            INT8 *squares();
        }

        Virtual::INT8 *Virtual::squares()
        {
            INT8 *ip = new INT8[1];

            for (size_t idx = 0; idx != sizeof(INT8) / sizeof(int); ++idx)
                (*ip)[idx] = (idx + 1) * (idx + 1);

            return ip;
        }
    }