C++ 템플릿(1)

은수·2022년 6월 6일

cpp study

목록 보기
8/21

C++ 템플릿 (template)

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

아래에 정의되는 class에 대해 템플릿을 정의하고, 템플릿 인자로 T를 받게되며, T는 반드시 어떠한 type의 이름임을 명시하고 있음.
위의 경우 템플릿 문장 아래 오는 것이 class Vector이므로 이 Vector 클래스에 대한 템플릿 명시

Vector<int> int_vec;

정의한 템플릿의 인자에 값을 전달하기 위해서는, 위와 같이 <> 안에 전달하려는 것 명시해줘야 함. 1번째 줄의 경우 Tint가 전달되는 것.

클래스 템플릿 인스턴스화 (class template instantiation)

클래스 템플릿에 인자를 전달해서 실제 코드를 생성하는 것

Vector<int>  // 혹은
Vector<std::string>

템플릿을 통해 인자로 타입 전달.
위와 같이 Vector의 템플릿의 인자에 타입을 전달하면, 컴파일러는 이것을 보고 코드 생성.

Vector <int>의 경우, 아래와 같이 Tint로 치환된 코드가 생성되는 것.

  class Vector {
  int* data;
  int capacity;
  int length;

 public:
  // 어떤 타입을 보관하는지
  typedef T value_type;

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

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

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

  // 임의의 위치의 원소에 접근한다.
  int 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;
    }
  }
};
Vector<int> int_vec;

즉, 위의 코드를 다시 보면 !
VectorTint로 치환된 클래스의 객체 int_vec를 생성하는 것.

  • 템플릿은 반드시 인스턴스화 되어야지만 컴파일러가 실제 코드를 생성

템플릿 특수화 (template specialization)

Vector<bool>과 같이 일부 경우에 대해서 따로 처리하는 것을 템플릿 특수화 라고 함.

  • C++은 기본적으로 처리하는 단위가 1byte
  • bool형은 1bit로 저장 가능하므로, 메모리 낭비.
  • 이런 특수한 상황에서 템플릿 특수화 사용
// 나는 A가 int고 C가 double일 때 따로 처리하고 싶어!
template <typename B>
class test<int, B, double> {};

// 나는 A, B가 int고 C가 double일 때 따로 처리하고 싶어!
// 이와 같이 전달하는 템플릿 인자가 없더라도 특수화 하고 싶다면
// template<> 라도 남겨줘야 함.
template <>
class test<int, int, double> {};

// 따라서, bool 벡터의 경우 아래와 같이 처리
template <>
class Vector<bool> {
  ...  // 원하는 코드
}

함수 템플릿 (Function template)

#include <iostream>
#include <string>

// 템플릿 함수 정의
template <typename T>
T max(T& a, T& b) {
  return a > b ? a : b;
}

int main() {
  int a = 1, b = 2;
  std::cout << "Max (" << a << "," << b << ") ? : " << max(a, b) << std::endl;

  std::string s = "hello", t = "world";
  std::cout << "Max (" << s << "," << t << ") ? : " << max(s, t) << std::endl;
}

실제로 템플릿 함수가 인스턴스화 되는 부분

  • max(a, b)가 호출되는 부분 = max<int>(a, b) 로 자동 해석
  • max(s, t)가 호출되는 부분 = max<string>(s, t)로 자동 해석
  • 참고 - 템플릿 메타프로그래밍 (template metaprogramming)
    템플릿으로 발생되는 오류는 프로그램이 실행되었을 때가 아니라 컴파일 할 때 발생. 컴파일 시에 모든 템플릿을 실제 코드로 변환하여 실행하기 때문.
    즉, 이와 같이 컴파일 시에 모든 템플릿들이 인스턴스화 되다는 사실을 바탕으로 코드를 짜는 방식을 템플릿 메타프로그래밍이라고 함.


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

함수는 아니지만 함수 인 척을 하는 객체를 함수 객체 (Function Object), 혹은 줄여서 Functor 라고 함

  • 클래스 자체에 여러가지 내부 state 를 저장해서 비교 자체가 복잡한 경우에도 손쉽게 사용자가 원하는 방식으로 만들어낼 수 있음
  • 함수포인터로 함수를 받아서 처리한다면 컴파일러가 최적화를 할 수 없지만, Functor 를 넘기게 된다면 컴파일러가 operator() 자체를 인라인화 시켜서 매우 빠르게 작업을 수행할 수 있음
template <typename Cont, typename Comp>

void bubble_sort(Cont& cont, Comp& comp) {
  for (int i = 0; i < cont.size(); i++) {
    for (int j = i + 1; j < cont.size(); j++) {
      if (!comp(cont[i], cont[j])) {
        cont.swap(i, j);
      }
    }
  }
}

여기서 comp 는 함수가 아니라 객체 이고, Comp 클래스에서 () 연산자를 오버로딩함.

struct Comp1 {
  bool operator()(int a, int b) { return a > b; }
};

struct Comp2 {
  bool operator()(int a, int b) { return a < b; }
};

Comp1 과 Comp2 모두 아무 것도 하지 않고 단순히 operator() 만 정의하고 있음. 그리고 이 객체들은 bubble_sort안에서 함수인양 사용됨 = 함수객체

함수 포인터와 함수 객체?


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

템플릿 인자로 타입만 받을 수 있는 것이 아님.

  • 템플릿 인자로 전달할 수 있는 타입
    - 정수 타입들 (bool, char, int, long 등등). 당연히 float 과 double 은 제외
    - 포인터타입
    - enum 타입
    - std::nullptr_t (널 포인터)

  • 타입이 아닌 템플릿 인자를 가장 많이 활용하는 예시는 컴파일 타임에 값들이 정해져야 하는 것들

  • 대표적으로 배열.

  • 템플릿 인자로 배열의 크기를 명시하여 함수에 배열을 전달할 때 배열의 크기 정보 전달

#include <iostream>

template <typename T, int num>
T add_num(T t) {
  return t + num;
}

int main() {
  int x = 3;
  std::cout << "x : " << add_num<int, 5>(x) << std::endl;
}

template 의 인자로 T 를 받고, 추가적으로 마치 함수의 인자 처럼 int num 을 또 받고 있음. add_num 함수를 호출할 때 <> 를 통해 전달하는 인자들이 들어가게 됨.

add_num<int, 5>(x) // 이렇게 전달했으므로

int add_num(int t) { return t + 5; } // 생성되는 함수는 이와 같음

디폴트 템플릿 인자

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

#include <iostream>

template <typename T, int num = 5>
T add_num(T t) {
  return t + num;
}

int main() {
  int x = 3;
  std::cout << "x : " << add_num(x) << std::endl;
}
  • 위의 코드와 같이, 템플릿 디폴트 인자는 함수 디폴트 인자와 같이 인자 뒤에 = (디폴트 값)을 넣어주면 됨.
  • 따라서 add_num(x) = add_num<int, 5>
#include <iostream>
#include <string>

template <typename T>
struct Compare {
  bool operator()(const T& a, const T& b) const { return a < b; }
};

template <typename T, typename Comp = Compare<T>>
T Min(T a, T b) {
  Comp comp;
  if (comp(a, b)) {
    return a;
  }
  return b;
}

int main() {
  int a = 3, b = 5;
  std::cout << "Min " << a << " , " << b << " :: " << Min(a, b) << std::endl;

  std::string s1 = "abc", s2 = "def";
  std::cout << "Min " << s1 << " , " << s2 << " :: " << Min(s1, s2)
            << std::endl;
}

  • 타입 역시 디폴트로 지정 가능
  • Comp 로 디폴트 타입인 Compare<T> 가 전달되어서 < 를 통해 비교를 수행




템플릿을 사용해서 임의의 개수의 인자를 받는 방법

가변 길이 템플릿

#include <iostream>

template <typename T>
void print(T arg) {
  std::cout << arg << std::endl;
}

template <typename T, typename... Types>
void print(T arg, Types... args) {
  std::cout << arg << ", ";
  print(args...);
}

int main() {
  print(1, 3.1, "abc");
  print(1, 2, 3, 4, 5, 6, 7);
}
```![](https://velog.velcdn.com/images/pesu1027/post/345bbeac-abfe-42a4-bb9e-7121d8f5e97e/image.png)

```cpp
template <typename T, typename... Types>
  • 템플릿 파라미터 팩(parameter pack)
    - typename 뒤에 ...으로 오는 것을 의미
    • 템플릿 파라미터 팩의 경우 0개 이상의 템플릿 인자들을 나타냄
    • ...이 옴 (?????)
void print(T arg, Types... args) {
  • 함수 파라미터 팩
    - 함수에 인자로 ...으로 오는 것을 의미
    • 0개 이상의 함수 인자를 나타냄
    • ...이 옴

파라미터 팩(parameter pack)

  • 파라미터 팩은 추론된 인자를 제외한 나머지 인자들을 나타냄.
  • 파라미터 팩은 0개 이상의 인자를 나타냄
  • C++ 규칙 상, 파라미터 팩이 없는 함수의 우선순위가 높음
print(1, 3.1, "abc");

C++ 컴파일러는 이 두 개의 print 함수 정의를 살펴보면서 어느 것을 택해야 할지 정해야 함. 첫 번째 print 의 경우 인자로 단 1 개만 받기 때문에 후보에서 제외되고 두 번째 print 가 택해짐.

template <typename T, typename... Types>
void print(T arg, Types... args) {
  std::cout << arg << ", ";
  print(args...);
}
  • print 의 첫 번째 인자는 1 이므로 Tint 로 추론되고, arg 에는 1 이 오게 됨. 그리고 args 에는 나머지 3.1 과 "abc" 가 오는 것!
  • print(args...);에 print에 전달되었던 나머지 인자들이 오는 것

** 참고

  • void print(T arg)void print(T arg, Types... args)보다 먼저 선언되어 있어야 함. void print(T arg, Types... args) 내의 print()가 앞의 함수에 정의되어 있기 때문에.
  • 즉, C++ 컴파일러는 함수를 컴파일 시에, 자신의 앞에 정의되어 있는 함수들만 볼 수 있음.

임의의 개수의 문자열을 합치는 함수

#include <cstring>
#include <iostream>
#include <string>

size_t GetStringSize(const char* s) { return strlen(s); }

size_t GetStringSize(const std::string& s) { return s.size(); }

template <typename String, typename... Strings>
size_t GetStringSize(const String& s, Strings... strs) {
  return GetStringSize(s) + GetStringSize(strs...);
}

// 재귀 호출 종료를 위한 베이스 케이스
void AppendToString(std::string* concat_str) { return; }

template <typename String, typename... Strings>
void AppendToString(std::string* concat_str, const String& s, Strings... strs) {
  concat_str->append(s);
  AppendToString(concat_str, strs...);
}

template <typename String, typename... Strings>
std::string StrCat(const String& s, Strings... strs) {
  // 먼저 합쳐질 문자열의 총 길이를 구한다.
  size_t total_size = GetStringSize(s, strs...);

  // reserve 를 통해 미리 공간을 할당해 놓는다.
  std::string concat_str;
  concat_str.reserve(total_size);

  concat_str = s;
  AppendToString(&concat_str, strs...);

  return concat_str;
}

int main() {
  // std::string 과 const char* 을 혼합해서 사용 가능하다.
  std::cout << StrCat(std::string("this"), " ", "is", " ", std::string("a"),
                      " ", std::string("sentence"));
}
  • GetStringSize 함수로 임의의 개수의 문자열을 받아서 각각의 길이를 더한 것들을 리턴
  • reserve 함수를 통해서 필요한 만큼 미리 공간을 할당
  • AppendToString 의 첫 번째 인자로는 합쳐진 문자열을 보관할 문자열을 계속 전달하고, 그 뒤로 합칠 문자열들을 인자로 전달

sizeof...

  • sizeof 연산자는 인자의 크기를 리턴
  • 파라미터 팩에 사용하는 sizeof... 연산자는 전체 인자의 개수를 리턴
#include <iostream>

// 재귀 호출 종료를 위한 베이스 케이스
int sum_all() { return 0; }

template <typename... Ints>
int sum_all(int num, Ints... nums) {
  return num + sum_all(nums...);
}

template <typename... Ints>
double average(Ints... nums) {
  return static_cast<double>(sum_all(nums...)) / sizeof...(nums);
}

int main() {
  // (1 + 4 + 2 + 3 + 10) / 5
  std::cout << average(1, 4, 2, 3, 10) << std::endl;
}

Fold 형식 (Fold expression)

가변 길이 템플릿은 매우 편리하지만 한 가지 단점 존재. -> 재귀 함수 형태로 구성해야 하기 때문에, 반드시 재귀 호출 종료를 위한 함수를 따로 만들어야 한다는 것 -> C++ 17 에 새로 도입된 Fold 형식을 사용한다면 이를 훨씬 간단하게 표현 가능

#include <iostream>

template <typename... Ints>
int sum_all(Ints... nums) {
  return (... + nums);
}

int main() {
  // 1 + 4 + 2 + 3 + 10
  std::cout << sum_all(1, 4, 2, 3, 10) << std::endl;
}

return (... + nums);

위의 코드가 Fold형식으로,

return ((((1 + 4) + 2) + 3) + 10);

컴파일러에서 이와 같이 해석됨. 이와 같은 형태를 단항 좌측 Fold (Unary left fold) 이라고 부름.

  • Fold 방식의 종류

    - I는 초기값을 의미
    • op자리에는 +, -, <, <<, -, >, ,와 같은 대부분의 이항연산자가 포함
    • 중요한 점은 Fold식을 쓸 때 꼭 ()로 감싸줘야 함.


0개의 댓글