using 선언(Using-declaration)

using 선언(Using-declaration)

네임스페이스나 기반 클래스에 정의된 이름을 다른 영역으로 가져오고 싶을 때가 있어요. using std::cout;처럼 쓰는 **using 선언(using-declaration)**은 다른 곳에 정의된 이름을, 이 using 선언이 나타나는 선언 영역(declarative region)으로 도입해요. 관련된 선언으로는 using enum(C++20부터)과 using namespace가 있어요.

출처: cppreference

본문

문법

using typename(optional) nested-name-specifier unqualified-id ; (until C++17)
using declarator-list ; (since C++17)
자리 의미
typename using 선언이 기반 클래스의 멤버 타입을 클래스 템플릿에 도입할 때, 의존 이름(dependent name)을 해석하려면 필요에 따라 typename 키워드를 쓸 수 있어요.
nested-name-specifier 이름들과 스코프 해석 연산자 ::의 시퀀스로, 스코프 해석 연산자로 끝나요. 단일 ::은 전역 네임스페이스를 가리켜요.
unqualified-id id 표현식
declarator-list typename(optional) nested-name-specifier unqualified-id 형태의 선언자 하나 이상을 쉼표로 구분한 목록. 일부 또는 모든 선언자 뒤에는 팩 확장(pack expansion)을 나타내는 생략 부호 ...가 올 수 있어요.

동작 방식(Explanation)

using 선언은 네임스페이스 멤버를 다른 네임스페이스와 블록 스코프로, 기반 클래스 멤버를 파생 클래스 정의로, 또는 열거자를 네임스페이스·블록·클래스 스코프로 도입하는 데 쓸 수 있어요. (C++20부터)

선언자가 둘 이상인 using 선언은 선언자가 하나인 using 선언들의 대응하는 시퀀스와 동등해요. (C++17부터)

네임스페이스·블록 스코프에서

using 선언은 다른 네임스페이스의 멤버를 현재 네임스페이스나 블록 스코프로 도입해요.

#include <iostream>
#include <string>

using std::string;

int main()
{
    string str = "Example";
    using std::cout;
    cout << str;
}

자세한 내용은 네임스페이스를 참고해요.

클래스 정의에서

using 선언은 기반 클래스의 멤버를 파생 클래스 정의로 도입해요. 예를 들어 기반의 protected 멤버를 파생의 public 멤버로 노출할 수 있어요. 이 경우 nested-name-specifier는 정의 중인 클래스의 기반 클래스를 가리켜야 해요. 이름이 기반 클래스의 오버로드된 멤버 함수의 이름이면, 그 이름을 가진 모든 기반 클래스 멤버 함수가 도입돼요. 파생 클래스에 이미 같은 이름·매개변수 목록·한정자를 가진 멤버가 있으면, 파생 클래스 멤버가 기반 클래스에서 도입된 멤버를 가리거나 오버라이드해요. (충돌하지 않아요.)

#include <iostream>
 
struct B
{
    virtual void f(int) { std::cout << "B::f\n"; }
    void g(char)        { std::cout << "B::g\n"; }
    void h(int)         { std::cout << "B::h\n"; }
protected:
    int m; // B::m은 protected
    typedef int value_type;
};
 
struct D : B
{
    using B::m;          // D::m은 public
    using B::value_type; // D::value_type은 public

    using B::f;
    void f(int) override { std::cout << "D::f\n"; } // D::f(int)는 B::f(int)를 오버라이드

    using B::g;
    void g(int) { std::cout << "D::g\n"; } // g(int)와 g(char) 모두 보임

    using B::h;
    void h(int) { std::cout << "D::h\n"; } // D::h(int)는 B::h(int)를 가림
};
 
int main()
{
    D d;
    B& b = d;
 
//  b.m = 2;  // 오류: B::m은 protected
    d.m = 1;  // protected B::m이 public D::m으로 접근 가능
    
    b.f(1);   // 파생 f() 호출
    d.f(1);   // 파생 f() 호출
    std::cout << "----------\n";
    
    d.g(1);   // 파생 g(int) 호출
    d.g('a'); // using B::g; 덕분에 노출된 기반 g(char) 호출
    std::cout << "----------\n";
    
    b.h(1);   // 기반 h() 호출
    d.h(1);   // 파생 h() 호출
}

출력:

D::f
D::f
----------
D::g
B::g
----------
B::h
D::h

생성자 상속(Inheriting constructors)

using 선언이 정의 중인 클래스의 직접 기반의 생성자를 가리키면(예: using Base::Base;), 파생 클래스를 초기화할 때 오버로드 해석에 그 기반의 모든 생성자(멤버 접근 무시)가 보이게 돼요.

오버로드 해석이 상속된 생성자를 선택하면, 대응하는 기반 클래스의 객체를 생성하는 데 쓰일 때 접근 가능하다면 그 생성자에 접근할 수 있어요. 그것을 도입한 using 선언의 접근성은 무시돼요.

오버로드 해석이 그런 파생 클래스 객체를 초기화할 때 상속된 생성자 중 하나를 선택하면, 생성자가 상속된 Base 하위 객체는 상속된 생성자로 초기화되고, Derived의 다른 모든 기반과 멤버는 기본 생성된 기본 생성자(defaulted default constructor)처럼 초기화돼요. (기본 멤버 초기화가 제공되면 그것을, 아니면 기본 초기화를 사용해요.) 전체 초기화는 단일 함수 호출로 취급돼요. 상속된 생성자의 매개변수 초기화는 파생 객체의 어떤 기반이나 멤버 초기화보다 먼저 순서대로 수행돼요.

struct B1 { B1(int, ...) {} };
struct B2 { B2(double)   {} };

int get();

struct D1 : B1
{
    using B1::B1; // B1(int, ...) 상속
    int x;
    int y = get();
};

void test()
{
    D1 d(2, 3, 4); // OK: B1이 B1(2, 3, 4) 호출로 초기화되고,
                   // 그 다음 d.x는 기본 초기화(초기화가 수행되지 않음),
                   // 그 다음 d.y는 get() 호출로 초기화
    D1 e;          // 오류: D1에는 기본 생성자가 없음
}

struct D2 : B2
{
    using B2::B2; // B2(double) 상속
    B1 b;
};

D2 f(1.0); // 오류: B1에 기본 생성자가 없음
struct W { W(int); };

struct X : virtual W
{
    using W::W; // W(int) 상속
    X() = delete;
};

struct Y : X
{
    using X::X;
};

struct Z : Y, virtual W
{
    using Y::Y;
};

Z z(0); // OK: Y의 초기화가 X의 기본 생성자를 호출하지 않음

Base 기반 클래스 하위 객체가 Derived 객체의 일부로 초기화되지 않을 경우(즉, BaseDerived의 가상 기반 클래스이고, Derived 객체가 가장 파생 객체가 아닌 경우), 상속된 생성자의 호출(어떤 인자의 평가 포함)은 생략돼요.

struct V
{
    V() = default;
    V(int);
};

struct Q { Q(); };

struct A : virtual V, Q
{
    using V::V;
    A() = delete;
};

int bar() { return 42; }

struct B : A
{
    B() : A(bar()) {} // OK
};

struct C : B {};

void foo()
{
    C c; // "bar"는 호출되지 않음. V 하위 객체가
         // B의 일부로 초기화되지 않기 때문
         // (V 하위 객체는 C의 일부로 초기화됨,
         //  "c"는 가장 파생 객체이므로)
}

생성자가 타입 Base의 여러 기반 클래스 하위 객체로부터 상속됐다면, 다중 상속된 비정적 멤버 함수와 비슷하게 프로그램은 잘못된 형태예요.

struct A { A(int); };
struct B : A { using A::A; };
struct C1 : B { using B::B; };
struct C2 : B { using B::B; };

struct D1 : C1, C2
{
    using C1::C1;
    using C2::C2;
};
D1 d1(0); // 잘못된 형태: 서로 다른 B 기반 하위 객체에서 상속된 생성자

struct V1 : virtual B { using B::B; };
struct V2 : virtual B { using B::B; };

struct D2 : V1, V2
{
    using V1::V1;
    using V2::V2;
};
D2 d2(0); // OK: B 하위 객체는 하나뿐
          // 이것이 가상 B 기반 클래스를 초기화하고,
          //   그것이 A 기반 클래스를 초기화한 뒤
          // V1과 V2 기반 클래스를 기본 생성된 기본 생성자처럼 초기화

다른 비정적 멤버 함수에 대한 using 선언처럼, 상속된 생성자가 Derived의 생성자 중 하나의 시그니처와 일치하면 Derived에서 찾은 버전이 검색에서 그것을 가려요. Base의 상속된 생성자 중 하나가 우연히 Derived의 복사/이동 생성자와 일치하는 시그니처를 가지면, Derived의 복사/이동 생성자의 암시적 생성을 막지 않아요. (그것이 상속된 버전을 가리고, using operator=과 유사해요.)

struct B1 { B1(int); };
struct B2 { B2(int); };

struct D2 : B1, B2
{
    using B1::B1;
    using B2::B2;
    
    D2(int); // OK: D2::D2(int)가 B1::B1(int)와 B2::B2(int)를 모두 가림
};
D2 d2(0);    // D2::D2(int) 호출

템플릿화된 클래스 안에서, using 선언이 의존 이름을 가리키면, nested-name-specifier의 끝 이름이 unqualified-id와 같을 때 생성자를 가리키는 것으로 간주돼요.

template<class T>
struct A : T
{
    using T::T; // OK, T의 생성자 상속
};

template<class T, class U>
struct B : T, A<U>
{
    using A<U>::A; // OK, A<U>의 생성자 상속
    using T::A;    // T의 생성자를 상속하지 않음
                   // T가 A<>의 특수화여도 그렇지 않음
};

(C++11부터)

스코프 있는 열거자 도입(Introducing scoped enumerators)

using 선언은 다른 네임스페이스의 멤버와 기반 클래스의 멤버에 더해, 열거형의 열거자를 네임스페이스·블록·클래스 스코프로 도입할 수도 있어요.

using 선언은 스코프 없는 열거자에도 쓸 수 있어요.

enum class button { up, down };

struct S
{
    using button::up;
    button b = up; // OK
};

using button::down;
constexpr button non_up = down; // OK

constexpr auto get_button(bool is_up)
{
    using button::up, button::down;
    return is_up ? up : down; // OK
}

enum unscoped { val };
using unscoped::val; // OK, 불필요하지만

(C++20부터)

주의할 점(Notes)

using 선언에서 명시적으로 언급된 이름만 선언 영역으로 옮겨져요. 특히, 열거형 타입 이름이 using 선언되면 열거자는 옮겨지지 않아요.

using 선언은 네임스페이스, 스코프 있는 열거자(until C++20), 기반 클래스의 소멸자, 또는 사용자 정의 변환 함수용 멤버 템플릿의 특수화를 가리킬 수 없어요.

using 선언은 멤버 템플릿 특수화를 이름으로 가리킬 수 없어요. (문법이 template-id를 허용하지 않아요.)

struct B
{
    template<class T>
    void f();
};

struct D : B
{
    using B::f;      // OK: 템플릿을 가리킴
//  using B::f<int>; // 오류: 템플릿 특수화를 가리킴
    
    void g() { f<int>(); }
};

using 선언은 의존 멤버 템플릿의 이름을 template-name으로 도입하는 데도 쓸 수 없어요. (의존 이름의 템플릿 명시자(template disambiguator)가 허용되지 않아요.)

template<class X>
struct B
{
    template<class T>
    void f(T);
};

template<class Y>
struct D : B<Y>
{
//  using B<Y>::template f; // 오류: 명시자 허용되지 않음
    using B<Y>::f;          // 컴파일되지만, f는 template-name이 아님
    
    void g()
    {
//      f<int>(0);          // 오류: f가 템플릿 이름으로 알려지지 않음,
                            // 그래서 <가 템플릿 인자 목록을 시작하지 않음
        f(0);               // OK
    }   
};

using 선언이 기반 클래스의 대입 연산자를 파생 클래스로 가져왔는데, 그 시그니처가 우연히 파생 클래스의 복사/이동 대입 연산자와 일치하면, 그 연산자는 파생 클래스의 암시적으로 선언된 복사/이동 대입 연산자에 가려져요. 상속된 기반 생성자가 파생 클래스의 복사/이동 생성자와 일치하는 경우에도 마찬가지가 적용돼요. (C++11부터)

생성자 상속의 의미론은 C++11에 대한 결함 보고로 소급 수정됐어요. 이전에는 상속 생성자 선언이 합성 생성자 선언의 집합을 파생 클래스에 주입했는데, 이는 불필요한 인자 복사/이동을 유발하고, 어떤 형태의 SFINAE와 문제 있는 상호작용을 하며, 어떤 경우엔 주요 ABI에서 구현 불가능할 수 있었어요. 오래된 컴파일러는 여전히 이전 의미론을 구현할 수 있어요.

using 선언이 정의 중인 클래스의 직접 기반의 생성자를 가리키면(예: using Base::Base;), 그 기반 클래스의 생성자가 다음 규칙에 따라 상속돼요.

struct B1
{
    B1(int);
};

struct D1 : B1
{
    using B1::B1;
    
    // 후보 상속 생성자 집합은
    // 1. B1(const B1&)
    // 2. B1(B1&&)
    // 3. B1(int)
    
    // D1은 다음 생성자를 가짐:
    // 1. D1() = delete
    // 2. D1(const D1&) 
    // 3. D1(D1&&)
    // 4. D1(int) <- 상속
};

struct B2
{
    B2(int = 13, int = 42);
};

struct D2 : B2
{
    using B2::B2;
    
    // 후보 상속 생성자 집합은
    // 1. B2(const B2&)
    // 2. B2(B2&&)
    // 3. B2(int = 13, int = 42)
    // 4. B2(int = 13)
    // 5. B2()
    
    // D2는 다음 생성자를 가짐:
    // 1. D2()
    // 2. D2(const D2&)
    // 3. D2(D2&&)
    // 4. D2(int, int) <- 상속
    // 5. D2(int) <- 상속
};

상속된 생성자는 빈 본문과, 모든 인자를 기반 클래스 생성자로 전달하는 단일 nested-name-specifier로 이루어진 멤버 초기화 목록을 가진 사용자 정의 생성자와 동등해요.

그것은 대응하는 기반 생성자와 같은 접근성을 가져요. 사용자 정의 생성자가 constexpr 생성자 요구사항을 만족했을 것이라면 constexpr이에요. 대응하는 기반 생성자가 삭제되었거나, 기본 생성된 기본 생성자가 삭제될 것이라면 삭제돼요. (단, 생성자가 상속되는 기반의 생성은 세지 않아요.) 상속 생성자는 명시적으로 인스턴스화되거나 명시적으로 특수화될 수 없어요.

두 using 선언이 (두 직접 기반 클래스에서) 같은 시그니처의 생성자를 상속하면 프로그램은 잘못된 형태예요.

상속 생성자 템플릿은 명시적으로 인스턴스화되거나 명시적으로 특수화되어서는 안 돼요.

과거의 상속 생성자 의미론
1) 후보 상속 생성자 집합은 다음으로 구성됨: a) 기반 클래스의 모든 비템플릿 생성자 (생략 부호 매개변수가 있으면 생략 후, C++14부터) b) 기본 인자나 생략 부호 매개변수가 있는 각 생성자에 대해, 생략 부호를 버리고 인자 목록의 끝에서 기본 인자를 하나씩 생략해 만든 모든 생성자 시그니처 c) 기반 클래스의 모든 생성자 템플릿 (생략 부호 매개변수가 있으면 생략 후, C++14부터) d) 기본 인자나 생략 부호가 있는 각 생성자 템플릿에 대해, 생략 부호를 버리고 인자 목록의 끝에서 기본 인자를 하나씩 생략해 만든 모든 생성자 시그니처 2) 기본 생성자나 복사/이동 생성자가 아니고, 시그니처가 파생 클래스의 사용자 정의 생성자와 일치하지 않는 모든 후보 상속 생성자가 파생 클래스에 암시적으로 선언됨. 기본 매개변수는 상속되지 않음:

using 선언에서의 팩 확장 덕분에, 재귀 없이 가변 기반들의 오버로드된 멤버를 노출하는 클래스를 만들 수 있어요.

template<typename... Ts>
struct Overloader : Ts...
{
    using Ts::operator()...; // 모든 기반의 operator() 노출
};

template<typename... T>
Overloader(T...) -> Overloader<T...>; // C++17 추론 가이드, C++20에선 불필요

int main()
{
    auto o = Overloader{ [] (auto const& a) {std::cout << a;},
                         [] (float f) {std::cout << std::setprecision(3) << f;} };
}

(C++17부터)

기능 테스트 매크로 표준 기능
__cpp_inheriting_constructors 200802L (C++11) 생성자 상속
__cpp_variadic_using 201611L (C++17) using 선언에서의 팩 확장

키워드

using

결함 보고(Defect reports)

이전에 발표된 C++ 표준들에 소급 적용된 동작 변경 결함 보고들이 있어요.

DR 적용 대상 발표 당시 동작 올바른 동작
CWG 258 C++98 파생 클래스의 비const 멤버 함수가 기반의 const 멤버 함수를 오버라이드/가릴 수 있었음 오버라이드와 가림도 cv-한정자가 같아야 함
CWG 1738 C++11 상속 생성자 템플릿의 특수화를 명시적으로 인스턴스화하거나 명시적으로 특수화하는 것이 허용되는지 불분명했음 금지됨
CWG 2504 C++11 가상 기반 클래스의 생성자 상속 동작이 불분명했음 명확해짐
P0136R1 C++11 상속 생성자 선언이 파생 클래스에 추가 생성자를 주입함 기반 클래스 생성자를 이름 검색으로 찾게 함

참고 자료(References)

  • C++23 표준 (ISO/IEC 14882:2024): 9.9 The using declaration [namespace.udecl]
  • C++20 표준 (ISO/IEC 14882:2020): 9.9 The using declaration [namespace.udecl]
  • C++17 표준 (ISO/IEC 14882:2017): 10.3.3 The using declaration [namespace.udecl]
  • C++14 표준 (ISO/IEC 14882:2014): 7.3.3 The using declaration [namespace.udecl]
  • C++11 표준 (ISO/IEC 14882:2011): 7.3.3 The using declaration [namespace.udecl]
  • C++03 표준 (ISO/IEC 14882:2003): 7.3.3 The using declaration [namespace.udecl]

더 알아보기

  • using namespace: 네임스페이스의 모든 멤버를 현재 스코프로 가져오는 지시(directive).
  • using enum: 열거자의 이름을 현재 스코프로 가져오는 선언.
  • 네임스페이스: 이름 충돌을 피하기 위한 이름 묶음.
  • 생성자 상속: using Base::Base;로 기반 생성자를 물려받는 방법.