본문 바로가기
NC University/Advanced C++

5일차 - 가변인자 템플릿 (variadic template) (2)

by 날쑤 2015. 8. 21.

가변인자 템플릿의 활용

  앞의 글에서는 가변인자 템플릿의 기본 문법들과 간단한 활용에 대해 다뤘다. 이번 글에서는 가변인자 템플릿을 조금 더 실용적으로 사용하고 있는 예제들을 살펴보고자 한다.

활용1 - xtuple

  예전 Template 부분 전문화(2)에서 Recursive Duo를 선형화하는 기술로 Tuple을 다뤘었다. 그 때의 Tuple은 Duo 템플릿을 기반으로 만들어졌기 때문에 겉보기에도 복잡하고, 결정적으로 최대 5개의 타입만을 저장할 수 있다는 문제가 있었다. 하지만 가변인자 템플릿을 이용하면 인자의 개수 제한도 없고, 예전보다 훨씬 단순한 구조의 tuple을 만들 수 있다. 그럼 가변인자 템플릿을 이용한 tuple을 만들어보자.

#include <iostream> 

using namespace std;  

template <typename T>  
void xtuple_size (const T& a) {  cout << T::N << endl;  }  

// tuple (primary template) 
template <typename ...Types> 
class xtuple;  

// empty tuple (specialization) 
template <> 
class xtuple<> {};  

// recursive tuple definition (partial specialization) 
template <typename T, typename ... Types>  
class xtuple<T, Types...> : private xtuple<Types ...> { 
public:   
	xtuple () {}   
	xtuple (const T& a, Types ... args) 
	: first_(a), xtuple<Types...>(args...) {}    
	
	enum { N = 1 + sizeof...(Types) };  

public:   
	T first_; 
};  

int main(int argc, char* argv[]) {   
	xtuple<int, char, double> t(1, 'a', 3.4);   
	cout << xtuple_size(t) << endl; //< 3    
	
	return 0; 
}

  이전 Tuple 클래스와 비교해서 상당히 심플해졌다. 이제 남은 것은 xtuple 안에 있는 요소에 접근하는 기능,즉 Get 함수다. 이 함수는 xtuple 하나를 인자로 받으며, 접근할 요소의 index를 템플릿 인자로 요구한다. 그리고 xtuple의 N번째 요소를 반환해야 하므로, return type은 이 요소의 타입이 되어야 한다.

  즉, Get 함수의 구현을 위해서는 xtuple과 index가 주어졌을 때, 여기에 대응하는 타입을 얻는 방법이 우선적으로 필요하다. 이를 위해서 아래와 같이 추가적인 템플릿 구조체를 정의한다.

// tuple_element 
template <size_t N, typename xtuple>  
struct xtuple_element;  

// type of the first element 
template <typename T, typename ... Types> 
struct xtuple_element<0, xtuple<T, Types...>> {   
	using Type = T;   
	using TupleType = xtuple<T, Types...>; 
};  

// recursive tuple_element definition 
template <size_t N, typename T, typename ... Types> 
struct xtuple_element<N, xtuple<T, Types...>> : public xtuple_element<N-1, xtuple<Types...>> {};

  Get 함수의 return type은 위의 템플릿 구조체를 이용하면 간단히 표현할 수 있다. 그런데 이 구조체는 return type 뿐만 아니라 한발짝 더 나아가 함수의 구현에도 큰 도움을 준다. 이제 이 템플릿을 이용해서 Get 함수를 완성해보자.

template <size_t N, typename ... Types>  
inline typename xtuple_element<N, xtuple<Types...>>::Type&  Get (xtuple<Types...>& t)  {   
	using TupleType = xtuple_element<N, xtuple<Types...>>::TupleType;   
	return ((TupleType&)t).first_; 
}

 

  xtuple을 상수 참조로 넘겼을 때에 대한 구현도 필요하지만 구현 방식은 동일하므로 const 버전의 코드는 생략한다. 구현을 살펴보면 Get 함수가 인자인 xtuple을 TupleType이라는 타입으로 캐스팅 한번 하는 것이 전부이다. 그냥 보면 이게 이해가 잘 안되는데, 구체적인 예를 놓고 생각해보면 조금 쉽다.

  위 코드에서 나온 xtuple_element<1, xtuple<int, char, double>>를 한번 고려해보자. 정의에 따라 이 구조체의 T는 char이고, TupleType은 부모 클래스인 xtuple<char, double>가 된다. 그러므로 위의 코드는 인자로 받은 xtuple을 부모 클래스로 캐스팅한 후 첫 번째 요소를 반환하는 코드이며, 이는 xtuple 자체가 첫 번째 element 뒤에 오는 요소들로 구성된 xtuple을 상속받는 방식으로 정의되어 있기 때문에 정상적으로 동작한다.

xtuple<int, char, double> t(1, 'a', 3.4);  
auto c = Get<1>(t);  
cout << c << endl; //< a

  마지막으로 위의 Get 함수를 이용해서 xtuple 내의 element들을 출력하는 함수를 소개하면서, xtuple에 대한 내용을 마무리하고자 한다.

// sequence template -> C++ 14에서는 std::index_sequence로 대체가능 
template <int... Remains> 
struct seq {};   

template <int N, int... Remains> 
struct gen_seq : gen_seq<N-1, N-1, Remains...> {};   

template <int... Remains> 
struct gen_seq<0, Remains...> : seq<Remains...> {};   

// pretty-print a tuple (from http://stackoverflow.com/a/6245777/273767) 
template <typename Tuple, int... Is> 
void print_tuple_impl (std::ostream& os, const Tuple& t, seq<Is...>) {   
	using swallow = int[]; 
	
	// guaranties left to right order   
	(void)swallow{0, (void(os << (Is == 0? "" : ", ") << Get<Is>(t)), 0)...}; 
}   

template <typename ... Types> 
std::ostream& operator<< (std::ostream& out, const xtuple<Types...>& t)  {   
	out << " [ ";   
	print_tuple_impl(out, t, gen_seq<sizeof...(Types)>{});   
	out << " ] "; 	   
	return out; 
}

 

활용2 - lockAndCall

  예전 글에서 perfect forwarding을 다루면서 lockAndCall 함수 템플릿이 등장했었다. 그런데 이 함수 템플릿은 전달함수 f의 인자가 하나일 때만 정상적으로 동작하는 한계가 있었다. 이를 가변인자 템플릿으로 수정하면 파라메터 개수에 제약이 없는 lockAndCall 함수를 만들 수 있다.

include <iostream> 

using namespace std;   

void foo (int a) {  cout << "foo" << endl;  } 
void goo (int a, int b) {  cout << "goo" << endl;  } 
void hoo () {  cout << "hoo" << endl;  }   

template<typename F, typename ... Types>  
void lockAndCall(F f, Types ... args) {   
	// mutex.lock();   
	f(args...);	//< pack expansion 
}   

int main(int argc, char* argv[]) {   
	lockAndCall(foo, 10);   
	lockAndCall(goo, 10, 20);   
	lockAndCall(hoo);    
	
	return 0; 
}

  main 함수에서 인자가 각각 0~2개인 함수를 lockAndCall로 전달해서 호출하고 있으며, 이들은 모두 정상적으로 동작한다. 하지만 아직 이 버전의 lockAndCall은 개선의 여지가 남아있다. 첫번째는 인자들의 perfect forwarding 문제이며, 두번째는 f의 return type이 void가 아닐 경우에 대한 처리이다. 이 문제에 대해서는 이후의 글에서 다룰 예정이다.