본문 바로가기

네임스페이스

같은 이름이 서로 부딪히지 않도록 이름 공간을 나누는 네임스페이스를 선언하고 :: 연산자로 출처를 지정하며, 이름 하나를 들여오는 using 선언과 이름 공간 전체를 검색에 더하는 using 지시문의 차이, 전역과 중첩, 익명 이름 공간의 범위를 다룹니다.

프로그램이 커지고 여러 라이브러리를 함께 쓰면, 서로 다른 곳에서 같은 이름의 함수나 변수를 정의하는 일이 생깁니다. 두 라이브러리가 모두 processData라는 함수를 가지고 있다면, 그 이름만으로는 어느 쪽을 부르는지 알 수 없습니다. 이름 공간(namespace)은 이름을 담는 공간을 나누어, 같은 이름이라도 속한 공간이 다르면 서로 부딪히지 않게 합니다. 글 제목처럼 영어를 그대로 읽어 네임스페이스라고도 부릅니다.

지금까지 써 온 std::cout의 std가 바로 표준 라이브러리의 이름 공간입니다. 이 글은 이름 공간을 직접 선언하고 그 안의 이름을 쓰는 방법, 접두사를 줄이는 using의 두 가지 모양과 그 위험, 전역과 중첩, 익명 이름 공간을 다룹니다.

이름 공간 선언과 :: 연산자

namespace 키워드 뒤에 이름을 적고 중괄호 안에 변수와 함수, 클래스를 선언하면, 그 이름들은 그 이름 공간에 속합니다. 바깥에서 그 이름을 쓸 때는 범위 지정 연산자(scope resolution operator) ::로 이름 공간을 앞에 붙입니다.

namespaces.cpp
#include <iostream>

namespace my_namespace {
    int data = 100;
    void printData() {
        std::cout << "my_namespace::data = " << data << "\n";
    }
}

namespace another_namespace {
    int data = 200;
    void printData() {
        std::cout << "another_namespace::data = " << data << "\n";
    }
}

int main() {
    std::cout << my_namespace::data << "\n";
    my_namespace::printData();
    std::cout << another_namespace::data << "\n";
    another_namespace::printData();
}
100
my_namespace::data = 100
200
another_namespace::data = 200

두 이름 공간은 모두 data와 printData를 가지지만, 속한 공간이 다르므로 서로 부딪히지 않습니다. 이름 공간 안의 printData는 같은 공간의 data를 접두사 없이 쓸 수 있고, 바깥의 main은 my_namespace::data처럼 출처를 밝혀 씁니다. 이름 공간의 정의는 함수 바깥에 둡니다.

using 선언과 using 지시문

이름 공간의 이름을 매번 접두사와 함께 쓰는 것이 번거로울 때는 using으로 줄일 수 있습니다. using에는 이름 하나를 들여오는 것과 이름 공간 전체를 들여오는 것, 두 가지 모양이 있습니다.

using_forms.cpp
#include <iostream>

namespace my_library {
    void funcA() { std::cout << "funcA\n"; }
    void funcB() { std::cout << "funcB\n"; }
}

int main() {
    using my_library::funcA;
    funcA();
    my_library::funcB();

    using std::cout;
    cout << "using std::cout\n";
}
funcA
funcB
using std::cout

using my_library::funcA;는 using 선언(using declaration)으로, 그 범위 안에서 funcA라는 이름 하나만 접두사 없이 쓸 수 있게 합니다. 같은 이름 공간의 funcB는 들여오지 않았으므로 여전히 my_library::funcB()로 불러야 하고, 그냥 funcB()라고 쓰면 그 이름을 찾지 못한다는 오류가 납니다. 자주 쓰는 이름만 골라 들여오므로 부딪힐 위험이 작습니다.

using namespace my_library;는 using 지시문(using directive)으로, 그 이름 공간의 모든 이름을 접두사 없이 찾을 때의 후보에 더합니다. 이름을 복사하는 것이 아니라 검색할 범위를 넓히는 것이어서, 두 이름 공간에 같은 이름이 있으면 둘 다 후보가 되어 어느 쪽인지 정할 수 없는 모호함이 생깁니다. my_library와 another_library가 모두 processData를 가진 상태에서 두 이름 공간을 모두 using namespace로 들여오면, processData()를 부르는 줄이 모호하다는 컴파일 오류가 납니다.

예제에서 자주 보는 using namespace std;도 같은 위험이 있습니다. std에는 count, distance, size처럼 흔한 이름이 아주 많아서, 직접 만든 이름과 부딪히기 쉽습니다. 특히 헤더 파일에 using namespace를 두면, 그 헤더를 포함하는 모든 파일에 이 위험이 퍼지므로 헤더에서는 쓰지 않습니다.

C++ · TYPE AND SCOPE

사용 범위가 좁을 때 이름 하나만 도입한다

한정 이름은 출처를 드러냅니다. 반복을 줄이려면 지역 범위의 using 선언을 검토합니다.

한정 이름을 기본으로 충돌을 제어한다사용 범위가 좁고 반복될 때만 using 선언을 쓰며 넓은 using 지시문은 피한다. 예아니요아니요예헤더·전역인가QUALIFIED완전 한정 이름넓은 using 지시문 피함한 이름이 반복?KEEP한정 이름 유지출처가 그대로 보임using 선언이름 하나만 도입함수·블록 범위로 제한
사용 범위와 반복에 따라 이름을 지정한다헤더나 전역이면 한정 이름을 쓰며 넓은using지시를 피한다. 아니면 한이름이 반복되는지 확인한다. 반복안되면 한정 이름유지,반복되면 지역using선언으로 이름하나를도입한다. 원desktop5노드4간선 두분기를모두보존한다.헤더 · 전역인가?한정 이름넓은 using 지시 피함한 이름이 반복?한정 이름 유지출처가 그대로 보임using 선언지역에서 이름만 도입예아니요아니요예
  1. 헤더·전역인가

    예: 완전 한정 이름을 쓰고 using directive를 피한다.

  2. 지역 범위인가

    이름 하나가 반복되는지 확인한다.

  3. 반복되지 않는가

    한정 이름을 유지해 출처를 보인다.

  4. 반복되는가

    필요한 이름 하나만 using 선언으로 지역에 도입한다.

한정 이름 유지

my_namespace::greet();
another_namespace::greet();

같은 이름인 두 함수의 출처를 직접 지정합니다.

지역 using 선언

void run() {
  using my_namespace::greet;
  greet();
}

필요한 greet만 함수 범위에 도입합니다. using namespace 지시문은 그 이름 공간을 비한정 검색 후보에 포함시키므로 충돌을 검토합니다.

사용 범위가 좁고 반복될 때만 using 선언을 쓰며 넓은 using 지시문은 피한다.

그림의 기준대로, 헤더나 전역 범위에서는 std::cout처럼 출처를 밝힌 한정 이름(qualified name)을 쓰고 넓은 using 지시문을 피합니다. 그 밖의 곳에서는 한 이름이 여러 번 되풀이될 때만 함수나 블록 안에서 using 선언으로 그 이름 하나를 들여오고, 그렇지 않으면 한정 이름을 그대로 둡니다.

더 깊이: using 없이 이름을 찾는 인자 의존 조회

std::cout << "Hi";는 using namespace std;가 없어도 컴파일됩니다. 여기서 쓰이는 <<는 std 안에 정의된 operator<<인데, 이 함수를 찾게 해 주는 규칙이 인자 의존 조회(ADL, argument-dependent lookup)입니다. 한정하지 않은 이름으로 함수를 부르면, 컴파일러는 보통의 범위뿐 아니라 인자의 타입이 속한 이름 공간에서도 그 이름을 찾습니다. 왼쪽 인자 std::cout의 타입이 std에 있으므로 std의 operator<<가 후보가 됩니다.

ADL은 일반적인 코드에서 일부러 쓰이기도 합니다. 템플릿에서 using std::swap;을 적은 뒤 swap(a, b)처럼 한정하지 않고 부르면, 인자의 타입이 자기 이름 공간에 swap을 정의해 두었을 때는 ADL로 그 함수가 쓰이고, 없을 때는 std::swap이 쓰입니다. std::swap(a, b)로 한정해 부르면 사용자가 만든 swap은 후보가 되지 않습니다. cppreference의 ADL 설명에 따르면 표준 라이브러리의 알고리즘도 원소를 맞바꿀 때 이 방식을 씁니다.

ADL이 일어나지 않는 경우도 있습니다. 보통의 이름 찾기가 클래스 멤버나, 같은 이름의 변수나 함수 객체처럼 함수가 아닌 이름을 먼저 찾으면 ADL은 하지 않습니다. C++20의 std::ranges::sort 같은 범위 알고리즘은 함수 객체로 만들어져 있어서, using namespace std::ranges; 뒤에 sort(v)처럼 불러도 ADL로 다른 이름 공간의 sort가 끼어들지 않습니다.

전역 이름 공간과 중첩 이름 공간

어떤 이름 공간이나 클래스, 함수 안에도 들어 있지 않은 최상위 선언은 전역 이름 공간(global namespace)에 속하며, 접두사 없이 쓸 수 있습니다. main 함수도 전역 이름 공간에 정의되지만, 그 안에서 선언한 변수는 함수의 블록 범위에 속합니다. 지역 변수가 전역 변수와 이름이 같아 가려질 때는 ::global_variable처럼 앞에 ::만 붙여 전역의 것을 가리킬 수 있습니다.

이름 공간은 다른 이름 공간 안에 중첩할 수 있으며, C++17부터는 namespace Outer::Inner { }처럼 한 줄로 중첩을 적을 수 있습니다.

C++ · TYPE AND SCOPE

범위 경계를 읽고 필요한 이름을 한정한다

전역·이름 공간·중첩 이름 공간·함수 블록은 서로 다른 선언 범위입니다.

이름 검색은 중첩된 범위 경계를 따른다전역, 이름 있는 이름 공간, 중첩 이름 공간, 블록 지역 범위를 안쪽으로 좁힌다. GLOBAL전역 이름 공간NAMESPACEapp 이름 공간NESTEDapp::metricsBLOCK함수 블록
이름 공간과 함수 블록의 포함 경계전역이 app이름공간을 포함하고 app은 metrics를 포함하며 그함수 블록을 포함한다. 지역count20과 이름공간count10은 서로 다른 객체이며 한정 이름은 출처를 지정한다. 화살표 없이 실제 포함 영역을 유지한다.전역 이름 공간app 이름 공간app::metrics함수 블록지역 count20metrics::count10영역 안쪽에 포함됨실행 순서를 뜻하지 않음
  1. 전역 이름 공간

    최상위 선언

  2. app 이름 공간

    관련 API 묶음

  3. app::metrics

    하위 도메인

  4. 함수 블록

    지역 변수와 using 선언

한정 이름은 범위를 직접 지정한다

namespace app::metrics {
  int count = 10;
  int read() {
    int count = 20;
    return app::metrics::count;
  }
}

함수 블록의 count는 20이지만, 한정한 app::metrics::count는 10입니다. namespace app::metrics 표기는 C++17부터 사용할 수 있습니다.

전역, 이름 있는 이름 공간, 중첩 이름 공간, 블록 지역 범위를 안쪽으로 좁힌다.

그림처럼 전역 이름 공간이 app 이름 공간을 품고, app이 app::metrics를 품으며, 그 안의 함수가 다시 블록 범위를 품습니다. 이 포함 관계는 실행 순서가 아니라, 어떤 이름이 어디에 선언되었는지를 나타냅니다.

nested_namespace.cpp
#include <iostream>

namespace app::metrics {
    int count = 10;

    int read() {
        int count = 20;
        return count + app::metrics::count;
    }
}

int main() {
    std::cout << app::metrics::count << ' ' << app::metrics::read() << "\n";
}
10 30

read 안의 지역 count는 20이고, app::metrics::count는 10인 다른 객체입니다. 이름 공간의 count는 함수 안에서 지역 count에 가려지지만, 한정 이름으로 출처를 밝히면 그 객체를 직접 가리킬 수 있습니다. 그래서 read()는 20 + 10인 30을 돌려줍니다.

익명 이름 공간

이름 없이 namespace { }로 선언한 익명 이름 공간(unnamed namespace) 안의 이름은 그 소스 파일 안에서만 쓸 수 있고, 다른 파일에서는 보이지 않습니다. 한 파일에서만 쓰는 도우미 함수나 변수를 이렇게 감싸 두면, 다른 파일에 같은 이름이 있어도 링크할 때 부딪히지 않습니다.

util.cpp
#include <iostream>

namespace {
    int internal_counter = 0;

    void internal_helper() {
        std::cout << "internal helper\n";
    }
}

void somePublicFunction() {
    ++internal_counter;
    internal_helper();
    std::cout << "internal_counter: " << internal_counter << "\n";
}

util.cpp에는 main이 없으므로 혼자서는 실행 파일이 되지 않습니다. 같은 폴더의 main.cpp에 void somePublicFunction();이라는 선언을 두고 main에서 이 함수를 두 번 부른 뒤, g++ -std=c++20 -Wall -Wextra main.cpp util.cpp -o util_demo처럼 두 파일을 함께 빌드해 실행하면 internal helper와 internal_counter: 1, 다시 internal helper와 internal_counter: 2가 출력됩니다. 다른 파일에서는 internal_counter나 internal_helper를 쓸 수 없습니다. C에서는 이런 이름 앞에 static을 붙여 파일 안으로 범위를 좁혔고 C++에서도 동작하지만, 익명 이름 공간은 클래스 같은 타입의 정의에도 쓸 수 있어 C++에서는 이쪽을 씁니다.

정리

  • 이름 공간은 이름을 담는 공간을 나누어, 속한 공간이 다르면 같은 이름이라도 부딪히지 않게 하며, 표준 라이브러리는 std에 있습니다.
  • 이름 공간의 이름은 이름공간::이름처럼 범위 지정 연산자로 출처를 밝혀 씁니다.
  • using 선언은 이름 하나만 들여오고, using 지시문은 이름 공간 전체를 검색 후보에 더하므로 같은 이름이 있으면 모호해집니다.
  • 헤더와 전역 범위에서는 using 지시문을 쓰지 않고, 되풀이되는 이름만 좁은 범위에서 using 선언으로 들여옵니다.
  • 이름 공간은 중첩할 수 있고 C++17부터 A::B로 한 번에 적을 수 있으며, 가려진 이름은 한정 이름으로 가리킵니다.
  • 익명 이름 공간의 이름은 그 소스 파일 안에서만 보이므로, 파일 안에서만 쓰는 이름을 감쌀 때 씁니다.

연습 문제

1. using 선언과 같은 이름

namespace A { int value = 10; }와 namespace B { int value = 20; }가 있습니다. main 안에서 using A::value; 다음에 std::cout << value << ' ' << B::value;를 실행하면 무엇이 출력되나요?

해설 보기

10 20이 출력됩니다. using A::value;가 A의 value만 들여왔으므로 접두사 없는 value는 A의 것이고, B의 것은 B::value로 출처를 밝혀 썼습니다.

2. 두 이름 공간을 모두 들여오면

같은 두 이름 공간에서 using A::value; 대신 using namespace A;와 using namespace B;를 두고 value를 쓰면 어떻게 되나요?

해설 보기

using 지시문은 두 이름 공간을 모두 검색 후보에 더하므로, 접두사 없는 value에 서로 다른 두 객체가 후보로 남아 모호하다는 컴파일 오류가 납니다. A::value나 B::value로 출처를 밝히면 해결됩니다.

3. 가려진 이름

본문의 nested_namespace.cpp에서 read 안의 return count + app::metrics::count;를 return count + count;로 바꾸면 read()는 무엇을 돌려주나요?

해설 보기

함수 안에서는 지역 count가 이름 공간의 count를 가리므로, 두 count가 모두 지역의 20을 뜻해 40을 돌려줍니다. 이름 공간의 값을 쓰려면 한정 이름으로 출처를 밝혀야 합니다.

이것으로 2장의 변수와 데이터 타입을 마칩니다. 다음 장의 산술 연산자에서는 덧셈과 나눗셈, 나머지, 증감 연산자가 피연산자의 타입에 따라 어떤 결과를 내는지와, 값이 바뀌는 시점을 다룹니다.