----------------
title : 씹어먹는 C++ - <9 - 2. 가변 길이 템플릿 (Variadic template)>
cat_title: 9 - 2. 가변 길이 템플릿 (Variadic template)
next_page : 221
publish_date : 2019-07-08
path : /C++
tex_title : 가변 길이 템플릿
--------------

이번 강좌에서는

* 가변 길이 템플릿 (variadic template)
* 파라미터 팩(parameter pack)
* Fold 형식 (Fold expression)

에 대해 다룹니다.

![](/img/ChewingCpplogo.png)

안녕하세요 여러분. 지난번 강좌에서 다룬 템플릿은 어떠셨나요? 템플릿을 잘 사용한다면 써야 되는 코드의 양을 비약적으로 줄일 수 있습니다. 이번 강좌는 그 연장선으로써, **템플릿을 사용해서 임의의 개수의 인자를 받는 방법**에 대해서 이야기 해보도록 할 것입니다.

### 가변 길이 템플릿

파이썬을 써보신 분들은 아시겠지만 파이썬의 경우 아래와 같이 `print` 함수를 이용하면 인자로 전달된 것들을 모두 출력할 수 있습니다.

```py
print(1, 3.1, "abc")
print("adfasf", var) 
```

그렇다면 C++ 에서도 이와 같은 기능을 구현할 수 있을까요? 재미있게도 C++ 템플릿을 이용하면 임의의 개수의 인자를 받는 함수를 구현할 수 있습니다. 바로 아래 예제를 보시지요.

```cpp-formatted
#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);
}
```

성공적으로 컴파일 하였다면

```exec
1, 3.1, abc
1, 2, 3, 4, 5, 6, 7
```

와 같이 잘 나옵니다. 그렇다면 위 코드가 어떻게 작동하는지 살펴보겠습니다.

```cpp-formatted
template <typename T, typename... Types>
```

먼저 위와 같이 `typename` 뒤에 ... 으로 오는 것을 **템플릿 파리미터 팩(parameter pack)** 이라고 부릅니다. 템플릿 파라미터 팩의 경우 0 개 이상의 템플릿 인자들을 나타냅니다.

```cpp-formatted
void print(T arg, Types... args) {
```

마찬가지로 함수에 인자로 `...` 로 오는 것을 **함수 파라미터 팩** 이라고 부르며, 0 개 이상의 함수 인자를 나타냅니다. 템플릿 파라미터 팩과 함수 파라미터 팩의 차이점은 템플릿의 경우 타입 **앞** 에 `...` 이 오고, 함수의 경우 타입 **뒤** 에 `...` 가 온다는 점입니다.

파라미터 팩은 추론된 인자를 제외한 나머지 인자들을 나타내게 됩니다. 예를 들어서

```cpp-formatted
print(1, 3.1, "abc");
```

위와 같은 `print` 함수 호출을 살펴보도록 합시다. C++ 컴파일러는 이 두 개의 `print` 함수 정의를 살펴보면서 어느 것을 택해야 할지 정해야 합니다. 첫 번째 `print` 의 경우 인자로 단 1 개만 받기 때문에 후보에서 제외되고 두 번째 `print` 가 택해집니다.

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

`print` 의 첫 번째 인자는 1 이므로 `T` 는 `int` 로 추론되고, `arg` 에는 1 이 오게 됩니다. 그리고 `args` 에는 나머지 `3.1` 과 "`abc`" 가 오게 됩니다.

```cpp-formatted
print(args...);
```

따라서 위 `args...` 에는 `print` 에 전달되었던 나머지 인자들이 쭈르륵 오게 되겠지요. 따라서 위 코드는 마치

```cpp-formatted
void print(int arg, double arg2, const char* arg3) {
  std::cout << arg << ", ";
  print(arg2, arg3);
}
```

을 한 것과 마찬가지로 됩니다. 자 그럼 이제 재귀적으로 다시 인자 2 개를 받는 `print` 를 호출하였습니다. 역시나 첫 번째 후보는 탈락하고, 두 번째 후보인 파라미터 팩을 받는 함수가 채택되어서 `T` 에는 `double` 이고 나머지 `Types...` 부분에는 `const char*` 이 들어가겠지요.

따라서 이를 통해 생성된 `print` 함수는

```cpp-formatted
void print(double arg, const char* arg2) {
  std::cout << arg << ", ";
  print(arg2);
}
```

와 같이 생겼을 것입니다.

```cpp-formatted
print(arg2);
```

자 그럼 이제 어떤 `print` 가 오버로드 될까요? 앞서 말했듯이 파라미터 팩은 0 개 이상의 인자들을 나타낸다고 하였습니다. 따라서

```cpp-formatted
template <typename T, typename... Types>
void print(T arg, Types... args);
```

위 함수도 가능하고 (이 경우 `args...` 에 아무것도 전달되지 않습니다. 즉 `print()` 가 호출됩니다.)

```cpp-formatted
template <typename T>
void print(T arg);
```

위도 가능합니다. 결론적으로 말하자면, 첫 번째 `print` 가 호출됩니다. 이는 C++ 규칙 상, 파라미터 팩이 없는 함수의 우선순위가 높기 때문입니다. 아무튼 덕분에 마지막에 `endl` 이 출력될 수 있었습니다.

#### 순서를 바꾼다면?

한 가지 재밌는 점은 두 `print` 함수의 위치를 바꿔서 쓴다면 컴파일 오류가 발생한다는 점입니다.

```cpp-formatted
#include <iostream>

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

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

int main() {
  print(1, 3.1, "abc");
  print(1, 2, 3, 4, 5, 6, 7);
}
```

컴파일 하였다면

```compiler-warning
test3.cc: In instantiation of ‘void print(T, Types ...) [with T = const char*; Types = {}]’:
test3.cc:7:8:   recursively required from ‘void print(T, Types ...) [with T = double; Types = {const char*}]’
test3.cc:7:8:   required from ‘void print(T, Types ...) [with T = int; Types = {double, const char*}]’
test3.cc:16:22:   required from here
test3.cc:7:8: error: no matching function for call to ‘print()’
   print(args...);
   ~~~~~^~~~~~~~~
test3.cc:5:6: note: candidate: template<class T, class ... Types> void print(T, Types ...)
 void print(T arg, Types... args) {
      ^~~~~
test3.cc:5:6: note:   template argument deduction/substitution failed:
test3.cc:7:8: note:   candidate expects at least 1 argument, 0 provided
   print(args...);
   ~~~~~^~~~~~~~~
```

위와 같은 오류가 발생하게 됩니다. 그 이유는 **C++ 컴파일러는 함수를 컴파일 시에, 자신의 앞에 정의되어 있는 함수들 밖에 보지 못하기 때문**입니다. 따라서 `void print(T arg, Types... args)` 이 함수를 컴파일 할 때, `void print(T arg)` 이 함수가 존재함을 모르는 셈이지요.

그렇게 된다면, 마지막에 `print("abc")` 의 오버로딩을 찾을 때, 파라미터 팩이 있는 함수를 택하게 되는데, 그 경우 그 함수 안에서 `print()` 가 호출이 됩니다. 하지만 우리는 `print()` 를 정의하지 않았기에 컴파일러가 이 함수를 찾을 수 없다고 오류를 뿜뿜 하게 되는 것입니다.

따라서 항상 템플릿 함수를 작성할 때 그 순서에 유의해서 써야 합니다.


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

가변 길이 템플릿을 활용한 또 다른 예시로 임의의 길이의 문자열을 합쳐주는 함수를 들 수 있습니다. 예를 들어서 `std::string` 에서 문자열을 합치기 위해서는

```cpp-formatted
concat = s1 + s2 + s3;
```

과 같이 해야 했는데, 잘 알다 시피 위는 사실

```cpp-formatted
concat = s1.operator+(s2).operator+(s3);
```

와 같습니다. 문제는 `s2` 를 더할 때 메모리 할당이 발생하고, `s3` 을 더할 때 메모리 할당이 또 한번 발생할 수 있다는 뜻입니다. 합쳐진 문자열의 크기는 미리 알 수 있으니가 차라리 한 번에 필요한 만큼 메모리를 할당해버리는 것이 훨씬 낫습니다.\sidenote{메모리 할당/해제는 매우 느린 작업 중 하나 입니다.}

```cpp-formatted
std::string concat;
concat.reserve(s1.size() + s2.size() + s3.size());  // 여기서 할당 1 번 수행
concat.append(s1);
concat.append(s2);
concat.append(s3);
```

를 하게 된다면 깔끔하게 메모리 할당 1 번으로 끝낼 수 있습니다. 그렇다면 위와 같은 작업을 도와주는 함수를 만든다면 어떨까요? 아래 처럼 말이지요.

```cpp-formatted
std::string concat = StrCat(s1, "abc", s2, s3);
```

을 한다면 깔끔하게 `concat` 에 `s1 + "abc" + s2 + s3` 한 문자열이 들어가게 됩니다. 물론 불필요한 메모리 할당이 없이 말이지요. 하지만 문제는 `StrCat` 함수가 임의의 개수의 인자를 받아야 된다는 것이지요. 여기서 바로 가변 길이 템플릿을 사용하면 됩니다.

#### 첫 번째 시도

```cpp-formatted
#include <iostream>
#include <string>

template <typename String>
std::string StrCat(const String& s) {
  return std::string(s);
}

template <typename String, typename... Strings>
std::string StrCat(const String& s, Strings... strs) {
  return std::string(s) + StrCat(strs...);
}

int main() {
  // std::string 과 const char* 을 혼합해서 사용 가능하다.
  std::cout << StrCat(std::string("this"), " ", "is", " ", std::string("a"),
                      " ", std::string("sentence"));
}
```

성공적으로 컴파일 하였다면

```exec
this is a sentence
```

와 같이 나옵니다. 위에서 파라미터 팩이 어떻게 작동하는지 이해하신 분들은 위 코드를 이해하기 쉬우실 것입니다. 우리의 `StrCat` 은 재귀적으로 정의되어 있는데;

```cpp-formatted
return std::string(s) + StrCat(strs...);
```

위에서 나머지 인자들을 합친 문자열과 현재 문자열(`s`) 를 더해주게 됩니다. 그리고 당연히도 재귀 호출의 베이스 케이스인

```cpp-formatted
template <typename String>
std::string StrCat(const String& s) {
  return std::string(s);
}
```

를 호출해야 하겠지요. `s` 는 `std::string` 으로 매번 감싸는 이유는 `s` 가 꼭 `std::string` 일 필요는 없기 때문이죠 (예컨대 `const char*` 일 수 도 있음)

하지만 위 구현은 문제가 있습니다. 위에서도 이야기 했듯이 결과적으로 `std::string` 의 `operator+` 를 매번 호출하는 셈이기 때문이지요. 따라서 `StrCat` 에 전달된 인자가 5 개라면 메모리 할당이 최대 5 번씩이나 일어날 수 있게되는 셈입니다.

효율적으로 `StrCat` 을 구현하기 위해서는 합쳐진 문자열의 길이를 먼저 계산한 뒤에, 메모리를 할당하고, 그 다음에 문자열을 붙이는 것이 좋을 것입니다.

#### 두 번째 시도

그렇다면 먼저 합쳐진 문자열의 길이를 먼저 구하는 함수를 만들어야 할 것입니다. 물론 이 역시 가변 길이 템플릿을 사용하면 매우 간단합니다.

```cpp-formatted
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...);
}
```

`GetStringSize` 함수는 그냥 임의의 개수의 문자열을 받아서 각각의 길이를 더한 것들을 리턴하게 됩니다. 참고로 `const char*` 과 `std::string` 모두 잘 작동하게 하기 위해서 인자 1 개만 받는 `GetStringSize` 의 오버로드를 각각의 경우에 대해 준비하였습니다.

그렇다면 수정된 `StrCat` 의 모습은 아래와 같을 것입니다.

```cpp-formatted
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;

  // concat_str 에 문자열들을 붙인다.
  AppendToString(&concat_str, strs...);

  return concat_str;
}
```

먼저 `GetStringSize()` 를 통해서 합쳐진 문자열의 총 길이를 계산한 뒤에, 합쳐진 문자열을 보관할 `concat_str` 이라는 변수를 만들었습니다. 그리고 `reserve` 함수를 통해서 **필요한 만큼 미리 공간을 할당해 놓죠**.

그 다음에는 이제 `concat_str` 뒤에 나머지 문자열들을 가져다 붙여야 합니다. 이 과정을 수행하는 함수를 `AppendToString` 이라고 해봅시다. 그렇다면 `AppendToString` 은 아래와 같이 구성할 수 있을 것입니다.

```cpp-formatted
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...);
}
```

`AppendToString` 의 첫 번째 인자로는 합쳐진 문자열을 보관할 문자열을 계속 전달하고, 그 뒤로 합칠 문자열들을 인자로 전달하게 됩니다. 그리고 재귀 호출의 맨 마지막 단계로 `strs...` 가 아무 인자도 없을 때 까지 진행하므로, 재귀 호출을 끝내기 위해선 `AppendToString(std::string* concat_str)` 을 만들어줘야 겠지요.

전체 완성된 코드를 보면 아래와 같습니다.

```cpp-formatted
#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"));
}
```

성공적으로 컴파일 하였다면

```exec
this is a sentence
```

와 같이 잘 나옵니다.

### sizeof...

`sizeof` 연산자는 인자의 크기를 리턴하지만 파라미터 팩에 `sizeof...` 을 사용할 경우 전체 인자의 개수를 리턴하게 됩니다. 예를 들어서 원소들의 평균을 구하는 함수를 생각해봅시다.

```cpp-formatted
#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;
}
```

성공적으로 컴파일 하였다면

```exec
4
```

와 같이 잘 구합니다. 코드를 살펴보자면;

```cpp-formatted
int sum_all() { return 0; }

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

`sum_all` 함수는 전달된 인자들의 합을 리턴하는 함수 입니다. 파라미터 팩을 이해하셨더라면 위 코드를 이해하는데 큰 문제가 없을 것입니다.

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

한편, `average` 함수의 경우 전달된 전체 인자 개수로 합을 나눠줘야만 합니다. 여기서 `sizeof...` 연산자가 활용됩니다. `sizeof...` 에 파라미터 팩 (`nums`) 를 전달하면 `nums` 에 해당하는 실제 인자의 개수를 리턴해줍니다. 우리의 경우 인자를 5 개 전달하였으므로 5 가 되었겠지요.


### Fold Expression

`C++ 11` 에서 도입된 가변 길이 템플릿은 매우 편리하지만 한 가지 단점이 있어야 합니다. 재귀 함수 형태로 구성해야 하기 때문에, 반드시 재귀 호출 종료를 위한 함수를 따로 만들어야 한다는 것이지요.

예를 들어서 위에서 만들었던 `sum_all` 함수를 다시 살펴보자면;

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

위와 같이 재귀 함수 호출을 종료하기 위해 베이스 케이스를 꼭 만들어줘야 한다는 점입니다. 이는 코드의 복잡도를 쓸데없이 늘리게 됩니다.

하지만 C++ 17 에 새로 도입된 Fold 형식을 사용한다면 이를 훨씬 간단하게 표현할 수 있습니다.

```cpp-formatted
#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;
}
```

성공적으로 컴파일 하였다면

```exec
20
```

과 같이 나옵니다.

```cpp-formatted
return (... + nums);
```

위 문장이 바로 C++ 17 에 추가된 Fold 형식으로, 위는 아래와 같이 컴파일러에서 해석됩니다.

```cpp-formatted
return ((((1 + 4) + 2) + 3) + 10);
```

위와 같은 형태를 **단항 좌측 Fold (Unary left fold)**라고 부릅니다. C++ 17 에서 지원하는 Fold 방식의 종류로 아래 표와 같이 총 4 가지가 있습니다. 참고로 `I` 는 초기값을 의미하며 파라미터 팩이 아닙니다.

|이름|Fold 방식|실제 전개 형태|
|---|--------|-----------|
| `(E op ...)`|단항 우측 Fold|$$(E_1 \text{ op } (...\text{ op } (E_{N-1} \text{ op } E_N))) $$|
| `(... op E)`|단항 좌측 Fold|$$(((E_1 \text{ op } E_2) \text{ op } ...) \text{ op } E_N) $$|
| `(E op ... op I)`|이항 우측 Fold|$$(E_1\text{ op } (...\text{ op } (E_{N-1}\text{ op } (E_{N}\text{ op } I))))$$|
| `(I op ... op E)`|이항 좌측 Fold|$$((((I\text{ op } E_1)\text{ op } E_2)\text{ op } ...)\text{ op } E_N$$|

여기서 `op` 자리에는 대부분의 이항 연산자들이 포함될 수 있습니다. 예를 들어서 `+`, `-`, `<`, `<<`, `->`, `,` 등등이 있습니다. 전체 목록은 [여기](https://en.cppreference.com/w/cpp/language/fold) 를 참조하시면 됩니다.

한 가지 중요한 점은 Fold 식을 쓸 때 꼭 `()` 로 감싸줘야 한다는 점입니다. 위 경우

```cpp-formatted
return (... + nums);
```

대신에

```cpp-formatted
return ... + nums;
```

로 컴파일 하게 된다면

```compiler-warning
test2.cc:6:10: error: expected primary-expression before ‘...’ token
   return ... + nums;
          ^~~
test2.cc:6:10: error: expected ‘;’ before ‘...’ token
test2.cc:6:10: error: expected primary-expression before ‘...’ token
```

위와 같은 오류가 발생하게 됩니다. (위 표에 `()` 가 Fold 식에 포함 되어 있는 것입니다!)

이항 Fold 의 경우 아래와 같은 예시를 들 수 있습니다.

```cpp-formatted
#include <iostream>

template <typename Int, typename... Ints>
Int diff_from(Int start, Ints... nums) {
  return (start - ... - nums);
}

int main() {
  // 100 - 1 - 4 - 2 - 3 - 10
  std::cout << diff_from(100, 1, 4, 2, 3, 10) << std::endl;
}
```

성공적으로 컴파일 하였다면

```exec
80
```

와 같이 나옵니다.

```cpp-formatted
return (start - ... - nums);
```

위 식은 위 표에 따르면 이항 좌측 Fold 입니다. 왜냐하면 `start` 가 초기값이고 `nums` 가 파라미터 팩 부분이기 때문이지요. 따라서 위 식은 실제로는 아래와 같이 컴파일 됩니다.

```cpp-formatted
return (((((100 - 1) - 4) - 2) - 3) - 10);
```

따라서 위 처럼 `80` 이라는 결과를 얻을 수 있겠지요.

한 가지 더 재미있는 점은 `,` 연산자를 사용하면 각각의 인자들에 대해 원하는 식을 실행할 수 있습니다.

```cpp-formatted
#include <iostream>

class A {
 public:
  void do_something(int x) const {
    std::cout << "Do something with " << x << std::endl;
  }
};

template <typename T, typename... Ints>
void do_many_things(const T& t, Ints... nums) {
  // 각각의 인자들에 대해 do_something 함수들을 호출한다.
  (t.do_something(nums), ...);
}
int main() {
  A a;
  do_many_things(a, 1, 3, 2, 4);
}
```

성공적으로 컴파일 하였다면

```exec
Do something with 1
Do something with 3
Do something with 2
Do something with 4
```

와 같이 나옵니다.

```cpp-formatted
(t.do_something(nums), ...);
```

위는 사실상 모든 인자들에 대해서 각각 `t.do_something(arg)` 를 실행한 것과 같습니다. 즉 실제 컴파일 되는 코드는

```cpp-formatted
t.do_something(1);
t.do_something(3);
t.do_something(2);
t.do_something(4);
```

가 되겠지요.

자 그럼 이것으로 이번 강좌를 마치도록 하겠습니다. 가변 길이 템플릿을 잘 활용한다면 작성해야 하는 코드의 양을 줄일 수 있습니다.

다음 강좌에서는 템플릿 메타프로그래밍이라는, 템플릿을 통해 생성된 코드로 프로그래밍을 하는 새로운 패러다임에 대해서 다룰 것입니다.

```lec-summary
파라미터 팩(`...`)을 사용해서 임의의 개수의 인자를 받는 템플릿을 작성할 수 있습니다.

C++ 17 에 새로 추가된 Fold 형식에 대해 배웠습니다.
```

##@ chewing-cpp-end