9-2. 가변 길이 템플릿(Variadic template)

가변 길이 템플릿(Variadic template)

  • 템플릿을 사용하면 가변 길이의 인자를 받을 수 있다.

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

파라미터 팩(parameter pack)

  • typename... 을 템플릿 파라미터 팩(parameter pack) 이라고 부른다. -> 템플릿 파라미터 팩의 경우 0개 이상의 템플릿 인자들을 나타낸다.

  • 마찬가지로 함수에 인자로 ... 로 오는 것을 함수 파라미터 팩 이라고 부른다. -> 템플릿 파라미터 팩과 마찬가지로, 0개 이상의 함수 인자를 나타낸다.

  • 다음과 같은 print 함수 호출을 생각해볼 때, C++ 컴파일러는 위에서 정의 된 2개의 print 함수 정의를 보면서 어떤 것을 사용할 것인지 선택한다. -> 첫번째 함수는 1개의 인자만 받기 때문에, 제외되고 두번째 함수를 선택하게 된다.

  • 따라서 위 코드는 다음과 같이 처리된다. 그 다음 재귀적으로 인자 2개를 받는 print 를 호출하였다.

  • 두번째 호출 된 print 함수는 다음과 같이 처리된다.

  • 마지막으로 print 함수는 첫번째 정의된 인자가 1개 있는 print 함수로 처리된다. -> 앞서 말했듯 파라미터 팩은 0개 이상의 인자들을 나타내기 때문에, 현재 정의된 두개 함수 모두 호출될 수 있다. -> 하지만 파라미터 팩이 없는 함수의 우선순위가 더 높기 때문에, 첫번째 함수가 호출된다 .

함수 정의 순서를 바꾼다면?

  • 위 코드에서 print 함수 정의 순서를 바꾼다면 컴파일 에러가 발생한다.

  • C++ 컴파일러는 함수를 컴파일 시에, 자신의 앞에 정의되어 있는 함수들 밖에 보지 못하기 때문이다. -> 따라서 void print(T arg, Types... args) 이 함수를 컴파일 할 때, void print(T arg) 이 함수가 존재함을 모르는 셈이다.

  • 그렇게 된다면, 마지막에 print("abc") 의 오버로딩을 찾을 때, 파라미터 팩이 있는 함수를 택하게 되는데, 그 함수 안에서 print() 가 호출되어 컴파일 에러가 발생한다. (print() 를 정의하지 않았다)

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

  • 가변 길이 템플릿을 활용한 또 다른 예시로, 임의의 길이의 문자열을 합쳐주는 함수를 들 수 있다.

  • 예를 들어서 std::string 에서 문자열을 합치기 위해서는 아래와 같이 사용해야 했다.

  • 위 코드는 사실 아래 코드와 동일하다.

  • 문제는 s2 를 더할 때 메모리 할당이 발생하고, s3 를 더할 때 메모리 할당이 또 한번 발생한다는 것이다. -> 합쳐진 문자열의 크기는 미리 알 수 있기에 차라리 한 번에 필요한 만큼 메로리를 할당해버리는 것이 훨씬 낫다.

  • 아래와 같이 코드를 변경한다면, 메모리 할당 1번으로 끝낼 수 있다.

  • 위와 같은 동작을 하는 함수를 다음과 같이 정의할 수 있지만, StrCat 함수가 임의의 개수의 인자를 받아야 된다는 것이 걸린다. -> 이 때, 가변 길이 템플릿을 사용하면 된다.

첫번째 시도

  • 첫번째 시도는 재귀 호출을 통해서 문자열을 계속 더하는 식의 시도를 할 수 있다. -> 위에서 파라미터 팩이 어떻게 동작하는지를 생각한다면 이해하기 쉽다.

  • 하지만 첫번쨰 시도는 하나의 문제가 있는데, 그것은 바로 std::stringoperator+ 를 매번 호출하는 셈이 되어, StrCat 의 전달된 인자가 5개라면, 메모리 할당이 최대 5번씩 일어날 수 있다는 것을 의미한다.

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

두번째 시도

  • 그렇다면 먼저 합쳐진 문자열의 길이를 구한는 함수를 만들어야 할 것이다. -> 물론 이 역시 가변길이 템플릿을 사용해보자

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

  • 그렇다면 수정된 StrCat 의 모습은 아래와 같을 것이다. -> GetStringSize 를 통해 확인 된 size 값을 사용해 concat_str 개체의 메모리 공간을 미리 할당한다.

  • 그 다음에는 이제 concat_str 뒤에 나머지 문자열들을 가져다 붙어야 한다. 이 과정을 수행하는 함수를 AppendToString이라고 하고 구현하면 다음과 같다.

  • 완성된 코드는 다음과 같다.

sizeof...

  • sizeof 연산자는 인자의 크기를 리턴하지만 파라미터 팩에 sizeof... 를 사용할 경우 전체 인자의 개수를 리턴하게 된다.

  • 예를 들어 원소들의 평균을 구하는 함수를 생각해보자.

Fold Expression

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

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

단항 Fold 형식

  • 아래 코드는 C++17 에서 추가된 Fold 형식으로,

  • 위 코드는 아래와 같이 컴파일러에서 해석된다.

  • 여기서 주의해야 할 점은 Fold 식을 사용할 때, 꼭 () 로 감싸주어야 한다는 것이다.

이항 Fold 형식

  • 아래 코드처럼 이항 Fold 형식을 사용할 수 있고,

  • 위 코드는 아래와 같이 컴파일러에서 해석된다.

Fold 의 추가적인 기능

  • , 연산자를 사용하면 각각의 인자들에 대해 원하는 식을 실행할 수 있다.

  • 아래 코드처럼 Fold 형식을 사용할 수 있고,

  • 위 코드는 아래와 같이 컴파일러에서 해석된다.

Last updated