9-1. 코드를 찍어내는 틀 - C++ 템플릿(template)

C++ 템플릿(template)

  • C++ 에서는 타입별로 코드를 만들어내는 반복을 줄이기 위해서 template 을 지원하고 있다.

  • 또한, 템플릿은 컴파일 타임에 타입과 관련된 오류를 체크할 수 있다.

  • vector 를 template 를 사용해 작성한 코드는 다음과 같다.

// 템플릿 첫 활용
#include <iostream>
#include <string>

template <typename T>
class Vector {
  T* data;
  int capacity;
  int length;

 public:
  // 생성자
  Vector(int n = 1) : data(new T[n]), capacity(n), length(0) {}

  // 맨 뒤에 새로운 원소를 추가한다.
  void push_back(T s) {
    if (capacity <= length) {
      T* temp = new T[capacity * 2];
      for (int i = 0; i < length; i++) {
        temp[i] = data[i];
      }
      delete[] data;
      data = temp;
      capacity *= 2;
    }

    data[length] = s;
    length++;
  }

  // 임의의 위치의 원소에 접근한다.
  T operator[](int i) { return data[i]; }

  // x 번째 위치한 원소를 제거한다.
  void remove(int x) {
    for (int i = x + 1; i < length; i++) {
      data[i - 1] = data[i];
    }
    length--;
  }

  // 현재 벡터의 크기를 구한다.
  int size() { return length; }

  ~Vector() {
    if (data) {
      delete[] data;
    }
  }
};

int main() {
  // int 를 보관하는 벡터를 만든다.
  Vector<int> int_vec;
  int_vec.push_back(3);
  int_vec.push_back(2);

  std::cout << "-------- int vector ----------" << std::endl;
  std::cout << "첫번째 원소 : " << int_vec[0] << std::endl;
  std::cout << "두번째 원소 : " << int_vec[1] << std::endl;

  Vector<std::string> str_vec;
  str_vec.push_back("hello");
  str_vec.push_back("world");
  std::cout << "-------- std::string vector -------" << std::endl;
  std::cout << "첫번째 원소 : " << str_vec[0] << std::endl;
  std::cout << "두번째 원소 : " << str_vec[1] << std::endl;
}
  • 일단 클래스 상단에 템플릿이 정의된 부분을 살펴보면 템플릿 인자로 T 는 반드시 어떠한 타입의 이름을 명시하고 있다.

  • 아래의 경우 클래스에 대한 템플릿을 명시하는데, 만약에 밑에 오는 것이 함수라면 함수에 대한 템플릿을 정의할 수 있다.

  • 간혹 템플릿 정의 시 아래와 같이 typename 이 아닌 class 를 사용하는 경우도 있는데, typename 을 사용하는 것을 권장한다. -> 기능은 정확히 동일하다.

  • C++ 을 처음 만들었던 사람은 템플릿 정의 시 새로운 키워드를 만들고 싶지 않아 class 를 사용했는데, 이후 시간이 흘러 C++ 위원회는 의미의 혼동을 막기 위해서 typename 이라는 키워드를 만들었다. ( class T 라고 하면 클래스 템플릿만 사용 가능한 것처럼 보여진다)

  • 템플릿을 사용할 때는 아래와 같이 템플릿에서 사용할 자료형을 명시해주면 해당 자료형을 템플릿에 전달하게 된다.

  • 아래와 같이 클래스 템플릿에 인자를 전달해서 실제 코드를 생성하는 것을 클래스 템플릿 인스턴스화(class template instantiation) 이라고 한다. -> 템플릿은 반드시 인스턴스화 되어야만 컴파일러가 실제 코드를 생성하게 된다.

  • 템플릿 클래스는 컴파일러에 의해서 중복된 코드를 더 많이 사용하기 때문에, 코드 크기와 메모리 사용량이 증가하게 된다.

템플릿 특수화

  • 특정 타입의 템플릿 사용에 대한 요구사항이 있을 때 템플릿 특수화를 통해 요구사항을 반영할 수 있다.

  • 예를 들어 아래와 같은 템플릿이 정의되어 있을 때,

  • A 는 int 고, C 가 double 일 때 일을 따로 처리하고 싶다면, 아래와 같이 같이 특수화 하고 싶은 부분에 원하는 타입을 전달하고, 나머지는 일반적인 템플릿을 사용할 수 있다.

  • 한가지 중요한 점은 전달하는 템플릿 인자가 없더라도 특수화 하고 싶다면 template<> 라도 남겨야 한다는 것이다.

1비트만 사용하는 bool 타입을 위해 템플릿 특수화를 사용해보자

  • Vector<bool> 을 구현하기 위해 int 배일을 사용해보자.

  • 1개의 int 는 4바이트 이므로, 32 개의 bool 데이터를 한데 묶어서 저장할 수 있다. -> 따라서 N 번째 bool 데이터는 N / 32 번째 int 안에 저장되어 있게 된다.

  • 이를 통해 bool 이 1바이트로 저장되는 것을 1비트로 저장할 수 있다. -> 메모리 관리 측면에서는 매우 효율이 높지만, 구현하는데는 조금 더 복잡하다.

...

함수 템플릿

  • 클래스가 아닌 함수에도 템플릿을 사용 가능하다.

  • 함수 템플릿 또한 함수가인스턴스화 되기 이전까지는 컴파일 시에 아무런 코드로 변환되지 않는다.

  • 위 코드를 살펴보면 함수를 아래와 같이 호출하는 것을 볼 수 있다.

  • 컴파일러는 생각보다 똑똑해서, a 와 b 의 타입을 보고서 알아서 max<int>(a, b) 로 인스턴스화 해준다.

함수 개체(Function Object - Functor) 의 도입

  • 아래 코드를 살펴보자 -> 아래 함수는 Comp 라는 클래스를 템플릿 인자로 받고, -> 함수 자체도 Comp 개체를 따로 받는다.

  • 아래 if 문에서 마치 함수를 호출하는 것 처럼 사용되는데, cont[i] 와 cont[j] 를 받아서 내부적으로 크기를 비교한 후 그 결과를 리턴하고 있다.

  • 한 가지 중요한 사실은 comp 는 함수가 아닌 개체이고, Comp 클래스에서 () 연산자를 오버로딩 한 버전이다.

  • 자세한 사용 코드는 아래와 같다.

  • 일단 아래 두 클래스에서 Comp1 과 Comp2 모두 아무 것도 하지 않고 단순히 operator() 만 정의하고 잇다.

  • 그리고 Comp1, Comp2 개체들은 bubble_sort 함수 안에서 마치 함수인양 사용된다. -> if(!comp(cont[i], cont[j])) {

  • 이렇게 함수는 아니지만, 함수 인 척을 하는 개체를 함수 개체(Function Object) or Functor 라고 부른다.

  • Functor 를 사용하는 방법이 코드를 하나하나 작성하는 방법보다 훨씬 편리한 점이 많다.

Functor 의 장점

  1. 상태 유지 : 함수 개체는 내부 상태를 유지할 수 있다. -> 함수 포인터나 람다식과 비교할 때 큰 장점이다.

  1. 더 나은 최적화 : 컴파일러는 함수 개체의 인라인화 같은 최적화를 더 잘 진행할 수 있다. 이는 함수 포인터와 비교할 때 성능상의 이점을 제공한다. -> operator() 자체를 인라인화 시켜 매우 빠르게 작업을 수행한다.

  1. ...

타입이 아닌 템플릿 인자(non-type template argument)

  • 템플릿 인자로 타입만 받을 수 있는 것은 아니다.

  • 템플릿 인자로 기본형 타입을 받을 수 있는데, 템플릿 사용시 명시적으로 어떤 값을 사용할 것인지 지정해 주어야 한다. -> 지정해주지 않는다면 컴파일 에러가 발생하는데, 컴파일러가 어떤 값이 들어가는지 알 수 없기 떄문이다.

  • 아래 코드는 사용 예시이다.

  • 타입이 아닌 템플릿 인자가 가장 많이 사용되는 예제는 컴파일 타임에 값들이 정해져야 하는 상황이다.

  • 이를 가장 잘 활용하는 예제는 배열이다.

    • C 스타일의 배열의 가장 큰 문제점은 함수에 배열을 전달할 때 배열의 크기에 대한 정보를 잃어버린다는 점이다.

    • 템플릿 인자로 배열의 크기를 명시한다면(어차피 배열의 크기는 컴파일 타임에 정해지는 것이다), 이 문제를 완벽하게 해결할 수 있다.

    • 이와 같은 기능을 가진 배열을 C++ 11 부터 제공하는 std::array 를 통해 사용할 수 있다.

  • 위 코드에서 재미있는 부분은 arr 은 런타임에 동적으로 크기가 할당되는 것이 아니라는 점이다. -> 컴파일 시에 int 5개를 가지는 메모리를 가지고 스택에 할당 된다. -> std::array 는 고정 크기의 배열을 포함하는 템플릿 클래스로 컴파일 타임의 배열의 크기가 결정된다.

  • 또한, 이 배열을 함수에 전달하기 위해서 그냥 std::array 를 받는 함수를 만들면 안된다. -> std::array<int, 5> 자체가 하나의 타입이다.

  • array 크기 별로 타입이 다르기 때문에, 크기 별로 함수를 만들어줘야 할 것 같은 상황에서, 아래와 같이 템플릿을 사용하면 유연하게 문제를 해결할 수 있다.

디폴트 템플릿 인자

  • 함수에 디폴트 인자를 지정하는 것처럼, 템플릿도 디폴트 인자를 지정할 수 있다.

  • 아래 코드는 add_num<int, 5> 를 한것과 동일하다. -> 참고로 int 는 컴파일러가 자동으로 추론해준 것이다.

Last updated