C++/[ecourse] C++ Template

8. 가변인자 템플릿

헛둘이 2022. 10. 6. 17:10
가변인자 템플릿
  • n개의 인자를 받는 템플릿
  • 0개일 수도 있고, 여러 개일 수도 있음
  • 인자와 타입의 개수가 정해지지 않은 형태의 템플릿

 

https://en.cppreference.com/w/cpp/utility/tuple

  • tuple의 경우 가변인자 클래스 템플릿으로 되어 있음
  • 인자를 0개를 줘도, 1개를 줘도, n개를 줘도 에러가 발생하지 않는다.
// 가변인자 클래스템플릿
template<typename ... Types>
class xtuple
{

};

// 가변인자 함수템플릿
template<typename ... Types>
void foo(Types ... args)
{

}
  • 타입의 경우 1개가 아니므로 관례상 Types로 통일
  • 받는 인자쪽도 arg가 아닌 복수형 args로 통일

 

Parameter Pack
  • C++11부터 지원
  • 가변인자를 받는 함수의 경우 Parameter Pack을 통해 args를 사용할 수 있다.
  • 인자가 몇 개 있는지 알고 싶으면 sizeof... 를 사용하면 됨
  • sizeof...에는 타입도 넣을 수 있다.
sizeof...(Types);
sizeof...(args);

 

 

Parameter Pack을 통해 다른 함수로 인자를 보낼 경우
  • goo(args) 이렇게 보낼 순 없으나 goo(args...)로 풀 수 있음
  • goo(args...) == goo(1, 3.4, "AAA")

 

 


Pack 심화

 

template<typename ... Types>
void foo(Types ... args)
{
	int x[] = { (++args)...}; // pack expansion

	for (auto n : x)
		std::cout << n << std::endl;
}


int main()
{
	foo(1, 2, 3);
}
  • int x[] = (++args)...; 는 ... 앞에 걍 파라미터 팩이 오는 게 아니라 파리미터팩을 사용한 패턴이 옴
  • int x[] = { hoo(args)... }; 이것은 함수를 이용한 패턴인데, ...을 함수 인자 내부에 쓰는 게 아닌 밖에 쓴다.
  • 함수 자체를 패턴이라고 생각하는 게 편할듯
int print(int a) {
	std::cout << a << std::endl;
	return 0;
}

template<typename ... T>
void test(T ... args)
{
	print(args)...;
	// 팩을 확장할땐 함수 인자에서 하거나, 배열 만들때 확장이 됨 {...}
	// Pack Extension은 아무데서나 할 수 있는게 아님

	int x[] = {0, print(args)...};
  	 // 인자가 없는 경우 이렇게 사용이 가능하다.
	
	int x[] = {0, (print(args), 0)... }; 
    	// print가 void를 리턴하는 경우 이렇게 사용하면 에러가 나지 않음
    
	initializer_list<int> e  = ~~
	// 전체 수식의 결과는 콤마 뒤에 있는 값을 쓰겠다는 뜻
	// 이니셜라이저리스트에서 받으면 아무것도 보내지 않아도 에러가 나지 않음


}

int main()
{
	test(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
}
  • 결과적으로 비교적 깔끔하지 않은데 C++17에서 지원하는 폴드 익스프레션을 통해 보완 가능함

 

 

 


Types에 대한 Pack Expansion
  • args 뿐만 아니라 Types도 Pack Expansion이 가능하다.
#include <iostream>
#include <algorithm>
#include <tuple>

template<typename ... Types>
void foo(Types ... args)
{
	// ...의 위치를 잘 파악해야 함
	std::pair<Types...> p1;
	std::tuple<Types...> t1;
	std::tuple<std::pair<Types...>> t2; // tuple<pair<int, double>> 이므로 문제 없음
	std::tuple<std::pair<Types>...> t3; // tuple<pair<int>, pair<double>> pair가 인자 2개를 받아야 하므로 error!
	std::tuple<std::pair<int, Types>...> t4; // tuple<pair<int, int>, pair<int, double>> 이상 없음!

	std::pair<std::tuple<Types...>> p2; // pair<tuple<int, double>> 인자가 1개이므로 error!
	std::pair<std::tuple<Types>...> p3; // pair<tuple<int>, tuple<double>> pair의 인자가 2개이므로 문제 없음
}

int main()
{
	foo(1, 3.4);
}
  • ...의 위치가 괄호 안이냐 밖이냐에 따라 어떻게 Expansion할 것인지 의미가 달라짐
// Types == (int, double)

// std::pair<Types...> 처럼 안에서 ...을 적어주면 안에서 Expansion
std::tuple<std::pair<Types...>> -> std::tuple<std::pair<int, double>>

// std::pair<Types>... 처럼 밖에서 ...을 적어주면 Types의 요소를 하나씩 ... 안에 담아서 Expansion 
std::tuple<std::pair<Types>...> -> std::tuple<std::pair<int>, std::pair<double>>

 

 

 

 

 

 

 


args의 각 요소를 꺼내는 방법
#include <iostream>
#include <algorithm>
#include <tuple>

template<typename ... Types>
void foo(Types ... args)
{
	//각 요소를 꺼내는 방법
	//int x[] = { args... }; //그러나 다른 타입이 있을 수 있으므로 이러면 안됨

	//tuple 사용법
	std::tuple<Types...> tp(args...);

	std::cout << std::get<0>(tp) << std::endl;
	std::cout << std::get<1>(tp) << std::endl;
	std::cout << std::get<2>(tp) << std::endl;
}

int main()
{
	foo(1, 3.4, "AAA");
}
  • tuple의 각 요소를 꺼내는 방법
  • std::get<인자위치>(tuple)을 사용해서 꺼낼 수 있다.

 


클래스 템플릿의 재귀호출
#include <iostream>
#include <algorithm>
#include <tuple>

void foo() {}

template<typename T, typename ... Types>
void foo(T value, Types ... args)
{
	std::cout << value << std::endl;

	// foo의 재귀호출
	foo(args...);
	//마지막 불리는건 템플릿 foo가 아니라 일반함수 foo
	// 이 방식의 단점은 함수가 많이 만들어짐

	// foo(1, 3.4, "AAA")
	// foo(3.4, "AAA")
	// foo("AAA")
	// foo() <- 일반함수 foo
}
  • foo에서 foo를 호출하는데 인자로 args만 넘기므로 현재 맨 앞의 인자를 뺀 나머지를 넘기는 꼴이 된다.
  • 계속 인자가 한 개씩 줄다가 마지막엔 foo()를 호출하는데 이는 5번째줄에 선언된 void foo() {}를 호출하게 됨
  • 이 방식의 단점은 불리는 foo가 인자가 다 다르므로 전부 다 다른 함수라고 봐야 함(함수 오버로딩)
  • 결국 다 다른 함수라는 뜻이므로 함수가 불필요하게 많아질 수 있음

 

 

 

 


Fold Expression
  • 이항연산자를 사용해서 Parameter Pack의 요소에 연산 수행
#include <iostream>
#include <algorithm>
#include <tuple>

void foo() {}

template<typename ... Types>
int foo(Types ... args)
{
	int x[] = { args... }; // pack expansion
	
	int n = (args + ...); // // c++17부터 지원되는 fold expression
	// 반드시 괄호로 묶어줘야 함
	
	int n = (... + args); // (((1 + 2) + 3) + 4)
	int n = (10 + ... + args); //초기값 10을 주는 형태 (((10 + 1) + 2) + 3) + 4
	// 반대도 마찬가지

	return n;
}

int main()
{
	int n = foo(1, 2, 3, 4);
	std::cout << n << std::endl;
}
  • 괄호를 통해 Parameter Pack을 언팩할 수 있다는 점에서 눈여겨볼 필요가 있음
  • C++17부터 지원
  • 위 코드를 응용해서 아래와 같이 사용할 수 있다.

 

template<typename ... Types>
void foo(Types ... args)
{
	(std::cout << ... << args);
}
  • std::cout은 초기값으로 놓고 연산자는 <<으로 처리
  • args의 모든 요소를 cout으로 출력함

 

 


  • 이전의 템플릿 특수화버전에선 n개의 인자를 갖는 함수만 특정해야 했었음
  • 근데 이렇게 하면 모든 인자에 대해 받을 수 있으므로 범용성이 더 좋음
template<typename T>
struct result_type
{
	typedef T type;
};

template<typename R, typename ... Types>
struct result_type<R(Types...)>
{
	typedef R type;
};

template<typename T>
void foo(const T& t)
{
	typename result_type<T>::type ret;

	std::cout << typeid(ret).name() << std::endl;
}

 

인자가 제대로 처리되지 않았을 때
  • 받는 인자가 함수가 아니라 정수나 실수의 경우 에러를 발생시키는 방법
template<typename T>
struct result_type
{
	static_assert(std::is_function<T>::value, "error");
};
  • Primary template에서  T를 검사해서 이렇게 처리할 수 있다.
  • 이외에도 primary template에서 선언만 제공해서 에러를 띄우는 경우들도 많이 보임
  • 흔히 사용하는 기법

 

 


함수 인자타입 구하기
double hoo(short a, int b, char ch) { return 0; }

template<size_t N, typename T>
struct argument_type
{
	typedef T type;
};

template<typename R, typename A1, typename ... Types>
struct argument_type<0, R(A1, Types...)>
{
	typedef A1 type;
};

template<size_t N, typename R, typename A1, typename ... Types>
struct argument_type<N, R(A1, Types...)>
{
	typedef typename argument_type<N-1, R(Types...)>::type type;
};


template<typename T>
void foo(const T& t)
{
	typename argument_type<2, T>::type ret;

	std::cout << typeid(ret).name() << std::endl;
}
  • 인자 타입을 구하는 방법
  • 기존 R(A1, A2)의 방법에서 가변인자를 적용해서 모든 함수타입에 적용 가능하도록 변경
  • 2를 넘기게 되면 아래와 같은 로직으로 진행됨
  • typedef typename argument_type<N-1, R(Types...)>::type type;에서 N-1이 0이 될 때까지 재귀호출
  • N-1하면서 Types만 넘기므로 N이 0이되는 순간 N번째 인자는 A1이 되고 0 특수화 버전에서 A1을 반환하게 됨