template 으로 container 전달받고 enable_if로 조건에 따라 특수화하기

오늘 코드를 작성하던 중 Serializer 관련 코드에서 vector, set, map 등의 컨테이너를 위한 read, write template 함수들이 선언되어 있는 것을 보게 되었습니다.

하지만 내부적인 구조는 value만 다루는 vector, deque, list, set 과 같은 컨테이너와 key, value를 함께 다루는 map, multimap, hash_map, unordered_map 같은 컨테이너 두 분류로 나눠져있고 중복된 코드가 보였습니다.

게다가 stdext::hash_map 만 특수화 되어 있어서 std::hash_map 등 특수화 되지 않은 컨테이너를 사용하려면 또 코드를 복사해서 특수화 하는 것을 반복해야 했습니다.

이거 왠지 template< template<class,class> class T> 표현으로 굳이 특수화를 매번 추가하지 않고도 다양한 컨테이너의 Serialize를 할 수 있겠다 싶어서 관련 코드를 대략 아래 코드와 동일한 접근 법으로 수정을 했습니다.

#include <iostream>
#include <vector>
#include <deque>

using namespace std;

// template 인자 두개를 받는 Container T를 받는 함수입니다.
template< template<class,class> class T, class V, class A>
void push_back( T<V,A>& con, V&& v) {
	con.push_back(v);
	con.push_back(v);
	con.push_back(v);
	con.push_back(v);
	con.push_back(v);
}

// vector, deque 모두 push_back 함수를 사용할 수 있습니다.
int main() {
	vector<int> v;
	deque<int> d;

	push_back(v, 1);
	push_back(d, 1);
	return 0;
}

여기서 새롭게 만난 문제는 Deserialize의 경우 vector는 size를 미리 알고 있기 때문에 reserve 함수를 호출해 주고 있었습니다. 그러면 reserve 함수를 가진 컨테이너와 아닌 컨테이너를 구분하여 특수화를 해줘야 하는데 아래의 코드와 같이 enable_if를 사용하고 member 함수의 존재를 확인해 주는 template class를 구현해 주면 가능합니다.

#include <iostream>
#include <vector>
#include <deque>
#include <type_traits>

using namespace std;

// 객체가 멤버함수 reserve를 가지고 있는지 확인합니다.
template<class T>
class has_member_reserve {
	using no  = char[1];
	using yes = char[2];
	
// SFINAE 규칙에 의해서 reserve 함수가 없으면 no를 반환합니다.
	template<class C>
	static yes&  test( decltype(&C::reserve));
	template<class C>
	static no&   test( ... );
public:
	static const bool value = sizeof(test<T>(nullptr)) == sizeof(yes);
};

// enable_if로 reserve 함수의 존재에 따라 적절한 push_back 함수를 호출합니다.
// 여기에서도 SFINAE 원칙이 적용됩니다.
template< template<class,class> class T, class V, class A>
void push_back( T<V,A>& con, V&& v, typename enable_if<has_member_reserve<T<V,A>>::value>::type* = nullptr) {
	con.reserve(5);
	con.push_back(v);
	con.push_back(v);
	con.push_back(v);
	con.push_back(v);
	con.push_back(v);
	cout << "this container has reserve member function" << endl;
}

template< template<class,class> class T, class V, class A>
void push_back( T<V,A>& con, V&& v, typename enable_if<has_member_reserve<T<V,A>>::value==false>::type* = nullptr) {
	con.push_back(v);
	con.push_back(v);
	con.push_back(v);
	con.push_back(v);
	con.push_back(v);
	cout << "this container does not have reserve member function" << endl;
}

int main() {
	vector<int> v;
	deque<int> d;
	
	push_back(v, 1);
	push_back(d, 1);
	return 0;
}

이제 사용자는 Container에 대한 특별한 제한을 느낄 수 없고 또 알아서 reserve까지 호출해 주는 좋은 코드를 사용할 수 있게 되었습니다.