C++공부/C++ Templates

5. 까다로운 기초 지식

아헿헿헿 2022. 6. 9. 17:23

5.1 키워드 typename

키워드 typename은 템플릿 내에서 식별자가 사실은 형식이라는 것을 명확히 하기 위해 C++ 표준화 단계에서 도입되었습니다. typename이 없다면 형식이 아닌 멤버로 간주될 수 있어 이에 대해 잘못된 코드를 생산하게 될 수도 있기에, 일반적으로 템플릿 파라미터에 종속된 이름이 형식일 경우에는 typename을 사용합니다. 일반 코드 내에서 표준 컨테이너의 반복자를 선언할 때가 그 예시가 될 수 있습니다.

5.2 0 초기화

기본형에는 기본값으로 초기화하는 기본 생성자가 없어, 지역 변수는 초기화되기 전까지 정해지지 않은 어떤 값을 가지게 됩니다. 기본값으로 초기화되는 템플릿형의 변수가 필요하다면, 내장 형식에 대해서 0으로 초기화하는 기본 생성자를 명시적으로 호출하는데 이를 값 초기화라고 합니다.

template<typename T>
void foo()
{
  T x{}; // x는 T가 내장 형식일 경우, 0을 갖는다. 
};

C++11 이전에는 T x= T(); 사용하였는데 이는 C++17 이전에는 복사 초기화를 위한 생성자가 explicit이 아닐 경우에만 이 방식이 동작하였습니다. C++11에서는 클래스에서 정적인 아닌 멤버에 대해서도 기본 생성이 제공됩니다. 하지만 이는 기본 인자에는 사용이 불가능하며 복사 생성으로 초기화 해야만 합니다.

5.3 this-> 사용

기본 클래스가 있는 클래스 템플릿에서 기본 클래스로부터 x를 상속받았다고 해도 x라는 이름이 항상 this->x를 의미하지는 않습니다. 이는 13장에서 더 자세하게 알아보겠습니다.

5.4 원시 배열과 문자열 리터럴을 사용하는 템플릿

문자열 리터럴이나 원시 배열일 템플릿으로 전달할 때 참조자로 선언한다면 형 소실이 되지않아, 형식이 그대로 들어가 문제가 될 수 있습니다. 문자열 리터럴을 위한 함수 템플릿을 위해서는 인자를 char const(&var)[N]의 형태로 받을 수 있으며, 크기가 알려지지 않은 배열을 위해서 부분 특수화를 수행할 수도 있습니다. 다음 프로그램은 모든 가능한 오버로딩을 제시합니다.

template<typename T>
struct MyClass;             // primary template

template<typename T, std::size_t SZ>
struct MyClass<T[SZ]>       // partial specialization for arrays of known bounds
{
  static void print() { std::cout << "print() for T[" << SZ << "]\n"; }
};

template<typename T, std::size_t SZ>
struct MyClass<T(&)[SZ]>    // partial spec. for references to arrays of known bounds
{
  static void print() { std::cout << "print() for T(&)[" << SZ << "]\n"; }
};

template<typename T>
struct MyClass<T[]>         // partial specialization for arrays of unknown bounds
{
  static void print() { std::cout << "print() for T[]\n"; }
};

template<typename T>
struct MyClass<T(&)[]>      // partial spec. for references to arrays of unknown bounds
{
  static void print() { std::cout << "print() for T(&)[]\n"; }
};

template<typename T>
struct MyClass<T*>          // partial specialization for pointers
{
  static void print() { std::cout << "print() for T*\n"; }
};


// 아래는 이를 직접적으로 사용하는 예시입니다.
template<typename T1, typename T2, typename T3>
void foo(int a1[7], int a2[],    // pointers by language rules
         int (&a3)[42],          // reference to array of known bound
         int (&x0)[],            // reference to array of unknown bound
         T1 x1,                  // passing by value decays
         T2& x2, T3&& x3)        // passing by reference
{
  MyClass<decltype(a1)>::print();     // uses MyClass<T*>
  MyClass<decltype(a2)>::print();     // uses MyClass<T*>
  MyClass<decltype(a3)>::print();     // uses MyClass<T(\&)[SZ]>
  MyClass<decltype(x0)>::print();     // uses MyClass<T(\&)[]>
  MyClass<decltype(x1)>::print();     // uses MyClass<T*>
  MyClass<decltype(x2)>::print();     // uses MyClass<T(\&)[]>
  MyClass<decltype(x3)>::print();     // uses MyClass<T(\&)[]>
}

int main()
{
  int a[42];
  MyClass<decltype(a)>::print();      // uses MyClass<T[SZ]>

  extern int x[];                     // forward declare array
  MyClass<decltype(x)>::print();      // uses MyClass<T[]>

  foo(a, a, a, x, x, x, x);
}

int x[] = {0, 8, 15};                 // define forward-declared array

배열로 선언된 호출 파라미터는 언어 규칙에 따라 실제로는 포인터 형에 해당합니다.

5.5 멤버 템플릿

클래스 멤버도 템플릿이 될 수 있으며, 중첩 클래스나 멤버 함수도 템플릿이 될 수 있습니다. 기본적으로 다른 형식으로 인스턴스화 된 클래스의 객체끼리의 형식 변환은 지원하지 않지만, 이를 템플릿 안에 명시한다면 가능해집니다. 이는 보통 private에 접근해야하기 때문에 이를 위해서는 다른 형식으로 인스턴스화된 클래스끼리 template<typename> friend class CLASSNAME로 설정하여 접근이 가능토록 해야합니다.(여기서 템플릿 파라미터의 이름이 사용되지 않기 때문에 생략 가능합니다.)

멤버 함수 템플릿의 특수화

멤버 함수 템플릿은 부분적으로나 전체적으로 특수화할 수 있습니다.

class BoolString {
  private:
    std::string value;
  public:
    BoolString (std::string const& s)
     : value(s) {
    }
    template<typename T = std::string>
    T get() const {         // get value (converted to T)
      return value;
    }
};

// bool 형을 위해 BoolString::getValue<>()에 대한 전체 특수화
template<>
inline bool BoolString::get<bool>() const {
  return value == "true" || value == "1" || value == "on";
}

특수 멤버 함수 템플릿

특수 멤버 함수가 객체의 복사나 이동을 허용하는 곳에서는 템플릿 멤버 함수를 쓸 수 있습니다.

때로는 멤버 템플릿을 호출할 때 명시적으로 템플릿 인자를 한정시키기 위하여 template 키워드를 통해 <가 템플릿 인자 목록의 시작이라는 것을 알립니다.

template<unsigned long N>
void printBitset(std::bitset<N> const& bs)
{
  std::cout << bs.template to_string<char, std::char_traits<char>,
    std::allocator<char >>();
};

C++14에서 도입된 일반 람다를 쓰면 멤버 템플릿을 더 빠르게 만들 수 있습니다

class SomeCompilerSpecificName {
  public:
    SomeCompilerSpecificName();
    template<typename T1, typename T2>
    auto operator() (T1 x, T2 y) const {
      return x + y;
    }
};

5.6 변수 템플릿

C++14부터는 변수조차도 특정 형식으로 파라미터화될 수 있습니다. 이를 변수 템플릿이라고 합니다.

template<typename T>
constexpr T pi{3.14};

이는 기본 템플릿 인자를 가질 수 있지만 항상 꺾쇠는 사용해 주어야 합니다. 변수 템플릿을 형식이 아닌 파라미터로도 파리미터화할 수 있습니다.

데이터 멤버를 위한 변수 템플릿

template<typename T>
class Myclass {
  public:
    static constexpr int max = 1000;
};

template<typename T>
int myMax = MyClass<T>::max;

auto i = Myclass<std::string>::max; // 원래대로 사용한 경우
auto i = myMax<std::string>;        // 코드 간략화됨

형식 특질 접미사가 이러한 방식으로 이루어져 있습니다.

std::is_const<T>::value // C++11
std::is_const_v<T>      // C++17

5.7 템플릿 템플릿 파라미터

템플릿 템플릿 파라미터를 사용한다면 요소의 형식에 대해 다시 명시하지 않고도 컨테이너형을 명시할 수 있습니다.

template<typename T, template<typename Elem> class Cont = std::deque>
class Stack {
  private:
    Cont<T> elems;             // elements
    //...
};

Stack<int, std::vector> vStack; // std::vector<int>에서 <int> 생략됨

C++11 이전에는 Cont는 클래스 템플릿의 이름으로만 치환될 수 있었지만, C++11에서부터는 별칭 템플릿 이름으로 치환이 가능해졌으며, C++17에서 부터는 템플릿 템플릿 파라미터 선언시 class 대신 typename이 사용가능 합니다. 여기서 Elem은 사용되지 않기에 생략 가능합니다.

 하지만, 위의 코드에서 std::deque가 템플릿 템플릿 파라미터 Cont에 대응 여부를 알아야 합니다. C++17이전에는 템플릿 템플릿 인자의 파라미터가 치환될 템플릿 템플릿 파라미터의 파라미터들과 정확히 일치해야 하기 때문에 생기는데, C++17부터는 기본 인자가 고려되지만, 이전에는 기본 값이 있는 인자를 명시하지 않는다면 일치할 수 없습니다. 예를 들면 std::deque에는 두번째 파라미터에 기본값이 있지만, C++17 이전에는 std::deque와 Cont 파라미터와의 일치 여부를 검사할 때 기본 값이 고려되지 않기에, 이를 명시하여 작성해주어야만 합니다.

template<typename T,
         template<typename Elem,
                  typename = std::allocator<Elem>>
          class Cont = std::deque>
class Stack {
  private:
    Cont<T> elems;             // elements
  ../
};

5.8 요약

  • 템플릿 파라미터에 종속된 형식의 이름에 접근하려면 typename을 앞에 붙여 이름을 한정해야 합니다.
  • 템플릿 파라미터에 종속된 기본 클래스의 멤버에 접근하려면 this->나 클래스 이름으로 접근을 한정해야 합니다.
  • 중첩 클래스와 멤버 함수도 템플릿일 수 있습니다. 내부 형식 변환을 쓰는 일반적인 연산을 구현하는 기술이 그 예입니다.
  • 템플릿 버전의 생성자나 할당 연산자는 사전 정의된 생성자나 할당 연산자를 대체하지 않습니다.
  • 중괄호를 사용한 초기화나 명시적으로 기본 생성자를 부르면 템플릿의 변수와 멤버가 내장 형식으로 인스턴스화됐다 하더라도 확실히 기본값으로 초기화할 수 있습니다.
  • 원시 배열에 대한 특정 템플릿을 제공할 수 있는데, 이 기법은 문자열 리터럴에도 적용할 수 있습니다.
  • 원시 배열이나 문자열 리터럴을 전달할 때 파라미터가 참조로 전달되지 않을 때에만 인자 연역 단계에서 인자의 형이 소실됩니다.
  • C++14부터 변수 템플릿을 정의할 수 있습니다.
  • 클래스 템플릿을 템플릿 템플릿 파라미터라 불리는 템플릿 파라미터로 쓸 수 있습니다.
  • 템플릿 템플릿 인자는 자기 파라미터와 정확히 일치해야만 합니다.