C++공부/C++ Templates

13. 템플릿에서 이름

아헿헿헿 2022. 6. 18. 11:40

13.1 이름 분류법

C++는 이름을 다양한 방식으로 분류합니다. 두가지 이름 개념에 대해 익숙해진다면 대부분의 C++ 템플릿과 관련된 문제는 쉽게 이해할 수 있습니다.

  • 한정된 이름(qualified name)이란 해당 이름이 속한 영역을 영역 해석 연산자나 멤버 접근 연산자를 사용해 명시적으로 알린 경우입니다.
  • 종속된 이름(dependent name)이란 템플릿 파라미터에 어떠한 방식으로든 종속돼 있는 경우입니다.

13.2 이름 룩업

 한정된 이름을 찾아볼 때는 한정된 생성에 의해 암묵적으로 알려진 영역만을 대상으로 합니다. 하지만 한정된 이름을 찾는 중에 해당 영역을 둘러싸는 외부 영역을 고려하지 않습니다.

 이에 비해 한정되지 않은 이름은 현재 영역을 둘러싼 더 큰 영역들을 차례로 룩업합니다. 이런 과정을 일반 룩업이라 합니다.

인자 종속 룩업

인자 종속 룩업(ADL, Argument Dependent Lookup)은 한정되지 않은 이름들에만 적용되는 법칙으로, 함수 호출이나 연산자 호출 시 멤버가 아닌 함수를 찾는 방식으로 생각할 수 있습니다. 일반 룩업이 다음을 찾는 다면 ADL은 일어나지 않습니다.

  • 멤버 함수의 이름
  • 변수의 이름
  • 형식의 이름
  • 블록 영역 함수 선언의 이름

ADL은 호출할 함수의 이름이 괄호로 둘러싸여 있을 경우에도 사용되지 않습니다.그 외의 경우 이름 뒤에 괄호로 둘러싸인 인자 표현식 목록이 따라온다면 ADL은 홏툴 인자의 형식과 연관된 네임스페이스와 클래스 안에서 이름을 계속해서 찾습니다. 특정 형식에 대한 연관된 네임스페이스와 연관된 클래스의 집합에 대한 정확한 정의는 다음과 같은 법칙에 따라 정해집니다.

  • 내장 형식이라면 빈 집합입니다.
  • 포인터와 배열형에서 연관된 네임스페이스와 클래스의 집합은 그 근간을 이루는 형식과 같습니다.
  • 열거형의 경우 선언된 네임스페이스를 의미합니다.
  • 클래스 멤버의 경우 둘러싼 클래스가 연관된 클래스입니다.
  • 클래스형의 경우, 자기 자신과 직간접적인 기본 클래스가 있다면 그들로 이루어집니다.
  • 함수형인 경우, 모든 파라미터형과 반환형에 관련된 클래스가 모두 포함됩니다
  • 클래스 멤버에 대한 포인터형의 경우, 클래스 및 멤버 형식에 대한 모든 것이 포함됩니다.

ADL은 모든 관련된 네임스페이스를 순서대로 방문하며 찾으려는 이름이 그 네임스페이스에 속한 것처럼 룩업합니다.

namespace X {
    template<typename T> void f(T);
}

namespace N {
    using namespace X;
    enum E { e1 };
    void f(E) { 
        std::cout << "N::f(N::E) called\n"; 
    }
}

void f(int) 
{ 
    std::cout << "::f(int) called\n"; 
}

int main() 
{
    ::f(N::e1);  // qualified function name: no ADL
    f(N::e1);    // ordinary lookup finds ::f() and ADL finds N::f(),
}                //  the latter is preferred

위의 경우 X::f는 후보조차 되지 못합니다.

프렌드 선언의 인자 종속 룩업

프렌드 함수 선언이 후보 함수에 대한 첫 번째 선언인 경우 클래스를 둘러싼 가장 가까운 네임스페이스에 해당 함수가 선언된 것으로 가정합니다. 하지만 이 함수가 클래스 내에서 선언된 경우, 클래스가 인스턴스화 되지 않았더라면 호출이 불가능합니다.

주입된 클래스 이름

클래스의 이름은 해당 클래스 자체 영역 내에 주입되며, 해당 영역 내에서는 한정되지 않은 이름으로 접근할 수 있습니다.

int C;

class C {
  private:
    int i[2]; 
  public:
    static int f() { 
        return sizeof(C); 
    }
};

int f() 
{ 
    return sizeof(C); 
}

int main() 
{
   std::cout << "C::f() = " << C::f() << ','
             << " ::f() = " << ::f() << '\n';
}

 C::f()에서는 클래스 C의 크기를 반환하지만, ::f()는 변수 C의 크기를 반환합니다. 클래스 템플릿의 경우 주입된 클래스 이름을 가지는데, 이름 뒤에 템플릿 인자가 따라 나올 수 있으며, 따라오지 않는 경우 문맥에서 형식을 기대한다면 자신의 파라미터 인자로 갖는 클래스를 나타내며, 문맥에서 템플릿을 기대하면 템플릿입니다.

template<template<typename> class TT> class X {};

template<typename T> class C{
  C* a;       // OK! C<T>* a와 동일
  C<void>& b; // OK!
  X<C> c;     // OK 템플릿 인자 목록이 없는 C는 템플릿 C를 나타냅니다.
  X<::C> d;   // OK! ::C는 주입된 클래스 이름이 아니므로 항상 템플릿을 나타냅니다.
};

한정되지 않은 이름이 주입된 이름을 참조하는 방식과 템플릿 인자 목록이 뒤따라오지 않는다면 한정되지 않은 이름을 템플릿의 이름으로 고려하지 않는다는 점에 주의해야합니다. 이를 보완하기 위해서는 파일 영역 한정자인 ::를 사용해 템플릿의 이름을 찾을 수 있게 강제해야 합니다.

현재 인스턴스화

클래스나 클래스 템플릿의 주입된 클래스 이름은 정의하고 있는 형식에 대한 별칭입니다. 클래스 템플릿 내에서 둘러싸고 있는 클래스나 클래스 템플릿의 주입된 클래스 이름이나 주입된 클래스 이름과 동등한 어떠한 형식을 현재 인스턴스화(current instantiation)라고 합니다. 템플릿 파라미터에 종속된 형식이지만 현재 인스턴스화를 가르키지 않는 형식은 알려지지 않은 특수화(unknown specialization)이라고 합니다. 즉, 알려지지 않은 특수화는 같은 클래스 템플릿에서 인스턴스화됐거나 완전히 다른 클래스 템플릿에서 인스턴스화된 특수화들을 말합니다.

 둘러싼 클래스와 클래스 템플릿의 주입된 클래스 이름은 현재 인스턴스화를 가리키는 데 반해 다른 중첩된 클래스나 클래스 템플릿의 이름은 그렇지 않습니다.

template<typename T> class C{
  using Type = T;
  
  struct I {
    C*       c;    // C는 현재 인스턴스화를 기리킨다.
    C<Type>* c2;   // C<Type>은 현재 인스턴스화를 가리킨다.
    I*       i;    // I는 현재 인스턴스화를 가리킨다.
  };
  
  struct J {
    C*       c;    // C는 현재 인스턴스화를 기리킨다.
    C<Type>* c2;   // C<Type>은 현재 인스턴스화를 가리킨다.
    C<T*>*   c3;   // C<T*>는 알려지지 않은 특수화를 가리킵니다.
    I*       i;    // I는 J를 포함하지 않기 때문에 I는 알려지지 않은 특수화를 가리킵니다.
    J*       j;    // J는 현재 인스턴스화를 가리킵니다.
  };
};

위의 예제에서 C<int>::J의 인스턴스화를 생각해보면, 이는 C<T>::J를 인스턴스화하는 것으로 이루어지는데, 명시적 특수화는 둘러싼 템플릿이나 멤버 모두를 특수화하지 않고서는 특수화가 불가능합니다. 따라서 C<int>는 둘러싼 클래스 정의에서 인스턴스화됩니다. 하지만 C<int>::I에 대해서는 명시적 특수화가 적용 가능합니다. 따라서 C<int>::I의 값이 C<T>::J의 정의에서 가시화된 것과는 완전히 다른 정의를 제공할 수 있기에, I에 해당하는 부분이 알려지지 않은 특수화를 가리키게 됩니다. 즉, 특수화로 인해 어떠한 정의가 될지 모르기에 알려지지 않은 특수화로 판단됩니다.

13.3 템플릿 파싱

대부분의 프로그래밍 언어를 위한 컴파일러의 기본 동작은 토큰화와 파싱입니다.

템플릿을 쓰지 않을 때의 문맥 민감성

X<1>(0) 은 X가 템플릿이라면 X<1>으로 인스턴스화된 클래스를 0으로 생성하는 것이지만, X가 값이라면, X<1을 수행하고나서 0과 크기 비교를 하는 문맥이 됩니다. 따라서 템플릿 인자 목록을 꺾쇠로 둘러싸는 것은 좋지 않습니다.

형식의 종속 이름

템플릿에서 이름과 관련된 문제는 이름들이 항상 잘 분류되지 않는데에서 기인합니다. 명시적 특수화로 인해 바뀔 수 있기에, 한 템플릿에서 다른 템플릿의 내부를 볼 수 없습니다. 따라서 알려지지 않은 특수화의 경우, 템플릿 내부를 들여다 볼 수 없기에, 우리가 의도한 대로 흘러간다는 보장을 할 수가 없습니다. 만약 특정 이름을 타입으로써 사용하려고 하였으나, 실제로는 변수일 수도 있다. 이러한 경우를 방지하기 위하여 언어 정의에서 키워드 typename을 사용하여 타입임을 명시할 수 있습니다. 이름 앞에 덧붙인 typename은 다음과 같은 상황의 이름에 꼭 필요합니다.(C++20에서는 거의 필요없어집니다)

  • 한정됐으나 ::이 자신 위에 덧붙여 좀 더 한정된 이름을 만들지 않을 때
  • 정교한 형식 명시자의 일부가 아닐떄(struct, class, union, enum)
  • 기본 클래스 명세 목록에 사용되지 않았거나 생성자 정의를 도입하는 멤버 초기화자 목록에 있지 않을 때
  • 템플릿 파라미터에 종속될 때
  • 알려지지 않은 특수화의 멤버일 때, 즉 한정자에 의해 이름 있는 형식이 알려지지 않은 특수화를 가리킬 때

템플릿의 종속 이름

일반적으로 C++ 컴파일러는 템플릿의 이름 뒤에 나오는 <를 보면 템플릿 인자 목록이 시작한다고 생각하지만, 비교 연산자일 수 있습니다. 형식 이름과 함께 <를 썼을 때 프로그래머가 template이라는 키워드를 프로그램에 직접 사용하지 않으면 컴파일러는 종속된 이름이 템플릿을 참조한다고 간주하지 않습니다.

template<typename T>
class Shell {
  public:
    template<int N>
    class In {
      public:
        template<int M>
        class Deep {
          public:
            virtual void f();
        };
    };
};

template<typename T, int N>
class Weird {
  public:
    void case1(typename Shell<T>::template In<N>::template Deep<N>* p) {
      p->template Deep<N>::f(); // 가상 호출을 억제한다.
    }
    void case2(typename Shell<T>::template In<N>::template Deep<N> p) {
      p.template Deep<N>::f(); // 가상 호출을 억제한다.
    }
};

한정 연산자(::, ->, .)가 앞에 나오는 이름이나 표현식의 형식이 템플릿 파라미터에 종속돼 있고 알려지지 않은 특수화를 가리키며, 연산자 다음에 나오는 이름이 템플릿 식별자인 경우 template 키워드가 필요합니다. C++ 컴파일러에게 Deep을 룩업할 때 이것이 템플릿인지를 명시해주어야 하기에 template를 붙입니다. 만약 없다면 p.Deep<N>::f()는 ((p.Deep)<N)>f()로 파싱됩니다.

using 선언에서 종속 이름

using 선언은 네임스페이스와 클래스라는 두 공간에 이름을 불러들입니다. using을 통해 이름을 쉽게 가져올 수 있지만, 앞서 말한 형식이 알려지지 않은 특수화에 해당할 수도 있기에 중첩된 이름의 경우 typename을 이용하여 타입임을 명시적으로 알려야 합니다.

template<typename T>
class BXT {
  public:
    using Mystery = T;
    template<typename U>
    struct Magic;
};

template<typename T>
class DXTM : private BXT<T> {
  public:
    template<typename U>
      using Magic = typename BXT<T>::template Magic<T>;
    Magic<T>* plink;
};

ADL과 명시적 템플릿 인자

namespace N {
  class X {
    ...
  };
  template<int I> void select(X*);
}

void g(N::X* xp)
{
  select<3>(xp);
}

위의 경우 select<3>(xp)가 ADL를 통해 템플릿 select를 찾는 것을 기대하였지만, <3>이 템플릿 인자 목록이라고 결정하기 전까지 xp가 함수 호출 인자라 결정할 수 없기에 ADL로 select()를 찾을 수 없습니다. 게다가 select()가 템플릿이라는 것을 알기 전까지 <3>을 템플릿 인자로 인식하지 않기에 위 표현식은 (select<3)>(xp)로 파싱이됩니다. 이를 위해서 select라는 이름의 함수 템플릿(template<typename T> void select())을 도입하면 이를 통해 select가 템플릿 함수라는 것을 인식하고, 그에 맞게 찾아가게 됩니다.

종속 표현식

표현식도 템플릿 파라미터에 종속될 수 있으며, 이러한 식은 인스턴스화에 따라 다르게 동작할 수 있습니다. 이와 달리 템플릿 파라미터에 종속되지 않은 표현식은 모든 인스턴스화에 대해 똑같이 동작합니다. 하지만 템플릿 파라미터에 관련된 모든 표현식이 형식에 종속되는 것은 아닙니다. 상수 값을 생성하는 템플릿 파라미터인 값 종속 표현식의 경우는 값에 종속됩니다. 값이나 형식에 종속되던 말던 간에 템플릿 파라미터와 관련된 표현식은 인스턴스화에 종속된 표현식으로 귀결됩니다. 이는 인스턴스화될 때 유효하지 않을 수도 있습니다.

13.4 상속과 클래스 템플릿

종속되지 않은 기본 클래스

클래스 템플릿에서 종속되지 않은 기본 클래스는 템플릿 인자를 알지 않아도 완전한 형식을 결정할 수 있는 클래스이기에 종속되지 않은 이름으로 지칭할 수 있습니다.

template<typename X>
class Base {
  public:
    int basefield;
    using T = int;
};

class D1 : public Base<Base<void>> {  // 이 클래스는 템플릿이 아님
  public:
    void f() { basefield = 3; }       // 상속받은 멤버로 일반적인 방식으로 접근
};

template<typename T>
class D2 : public Base<double> {      // 종속적이지 않은 기본 클래스
  public:
    void f() { basefield = 7; }       // 상속받은 멤버로 일반적인 방식으로 접근
    T strange;                        // 여기서 T는 Base<double>::T에 해당
};

 한정되지 않은 이름을 템플릿화된 파생에서 찾아볼 때는 템플릿 파라미터의 목록들보다 먼저 종속적이지 않은 기본 클래스들을 고려하게 됩니다.

종속적인 기본 클래스

표준 C++은 템플릿 내에서 나온 종속적이지 않은 이름은 나온 즉시 룩업해야한다고 명시하지만, 종속적이지 않은 이름은 종속적인 기본 클래스에서 룩업하지 않는다고 정했습니다. 따라서 컴파일러 단계에서 오류를 내는데 이를 위해 인스턴스화될 때까지 이름에 대한 룩업을 지연시키는 방법을 수행합니다. 이는 this->를 사용하거나 한정된 이름을 통해 종속 관계를 도입하는 방법이 존재합니다. 또한 종속 기본 클래스의 이름을 파생 클래스로 가져와 계속 사용하는 방법도 존재합니다.d