KoreanFoodie's Study

C++ 기초 개념 9-2 : 가변 길이 템플릿(variadic template), 파라미터 팩(parameter pack), Fold 형식(Fold expression) 본문

Tutorials/C++ : Beginner

C++ 기초 개념 9-2 : 가변 길이 템플릿(variadic template), 파라미터 팩(parameter pack), Fold 형식(Fold expression)

GoldGiver 2022. 1. 14. 19:39

모두의 코드를 참고하여 핵심 내용을 간추리고 있습니다. 자세한 내용은 모두의 코드의 씹어먹는 C++ 강좌를 참고해 주세요!

가변 길이 템플릿

C++ 템플릿을 이용하면 파이썬처럼 임의의 갯수의 인자를 받는 함수를 구현할 수 있다.

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

}

 

typename... Types 에서 ... 으로 표현된 부분을 템플릿 파라미터 팩(parameter pack)이라고 하고, print 함수에서 Types... args 부분을 함수 파라미터 팩이라고 부른다. 이는 각각 0개 이상의 템필릿 인자나 함수 인자를 받는다는 의미이다.

 

위의 코드를 잘 보면, 재귀적으로 동작함을 알 수 있다.

즉, print(1, 3.1, "abc") 는 다음과 같이 분해된다.

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

 

이때, 1, 3.1, "abc" 에 맞는 타입이 템플릿에서 자동으로 추론된다. 이후, 다시 재귀적으로 함수가 호출되고, 마지막으로 인자가 하나가 남았을 때에는 void print(T arg) 가 호출되어, std::endl; 이 잘 출력되는 것이다.

 

순서를 바꾼다면?

만약 두 print 함수의 위치를 바꾸면 컴파일 오류가 발생한다. 왜냐하면 C++ 컴파일러는 함수를 컴파일 시에, 자신의 앞에 정의되어 있는 함수들밖에 보지 못하기 때문이다. 즉, 가변 길이 템플릿을 사용한 print 함수가 마지막으로 인자가 하나인 함수를 호출해야 하는 단계에서( print("abc") 를 해야함), print( ) 가 호출된다. 그런데 print( ) 는 정의되지 않았으므로, 오류가 발생하는 것이다!

따라서 템플릿 함수 작성 시에는 순서를 잘 지켜 주어야 한다.

 

 

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

concat = s1 + s2 + s3;

같은 표현은 s2와 s3 를 더할 때 각각 메모리 할당하는 문제가 있다.

 

가변 길이 템플릿을 이용해 위의 문제점을 해결하면서 임의의 갯수의 문자열을 합치는 함수를 만들어보자.

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

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

size_t getSize(const char* s) { return strlen(s); }
size_t getSize(const std::string& s) { return s.size(); }

template <typename String, typename... Strings>
size_t getSize(const String& arg, Strings... args) {
  return (getSize(arg) + getSize(args...));
}

void Concat(std::string& ret) { return; }

template <typename String, typename... Strings>
void Concat(std::string& ret, const String arg, Strings... args) {
  Concat(ret.append(arg), args...);
}

template <typename String, typename... Strings>
std::string Concatenate(const String& s, Strings... args) {
  int total_size = getSize(s, args...);
  
  std::string ret;
  ret.reserve(total_size);

  ret = s;

  Concat(ret, args...);

  return ret;
}


int main() {

  std::cout << Concatenate("1", std::string("2"), "3", std::string("4")) << std::endl;

}

 

 

sizeof...

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

#include <iostream>

template <typename Num>
Num sum(Num a) { return a; }

template <typename Num, typename... Nums>
Num sum(Num a, Nums... nums) {
	return (a + sum(nums...));
}

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


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

 

위의 average 함수를 보면, sizeof... 를 통해 가변 길이 변수의 개수를 구하고 있다.

 

 

Fold Expression

C++11에 도입된 가변 길이 템플릿은 다음과 같은 코드처럼 재귀 함수 호출을 종료하는 코드를 만들어 주어야 한다.

template <typename Num>
Num sum(Num a) { return a; }

 

C++17에 도입된 Fold 형식을 표현하면 이를 훨씬 간단히 표현할 수 있다.

#include <iostream>

template <typename... Nums>
int sum(Nums... nums) {
    return (... + nums);
}

int main() {
    std::cout << sum(1, 4, 2, 3, 10) << std::endl;
    
    // 다음과 같이 해석됨
    // return ((((1 + 4) + 2) + 3) + 10);
}

 

위와 같은 형태를 단항 좌측 Fold (Unary left fold)라고 부른다. C++17 에서 지원하는 Fold 방식의 종류로 아래 표와 같이 4가지가 있다. (I 는 초기값을 의미)

 

Fold 식을 쓸때는 반드시 ( ) 로 감싸주어야 한다. 왜냐하면 ( ) Fold 식에 포함되어 있기 때문이다. 따라서 아래와 같은 코드는 오류를 발생시킨다.

return ... + nums;

 

이항 Fold 의 다른 예시를 보자.

#include <iostream>

template <typename Num, typename... Nums>
Num diff(Num n, Nums... nums) {
	return (n - ... - nums);
}


int main() {
	std::cout << diff(100, 1, 4, 2, 3, 10) << std::endl;
}

 

Fold Expression을 이용해 함수를 호출할 수도 있다.

#include <iostream>

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

template <typename T, typename... Nums>
void do_many_things(const T& t, Nums... nums) {
	(t.do_something(nums), ...);
}

int main() {
	A a;
	do_many_things(a, 1, 3, 2, 5);
}

 

따라서 출력은 다음과 같다.

Do something with 1
Do something with 3
Do something with 2
Do something with 5

// 아래 코드를 실행한 것과 같음
t.do_something(1);
t.do_something(3);
t.do_something(2);
t.do_something(5);

 

 

 

Comments