2015년 6월 17일 수요일

C/C++ , 좋은 프로그래밍 - 템플릿 - 3


이제 분기문에 대해서 이야기 해보자.
네트워크 프로그램을 제작한다고 상상해보자. 보통의 처리 방법은 모든 패킷에 번호를 부여하여 그 번호에 따라 실행할 구문을 정한다.


switch(packet_type)
{
case PACKET_TYPE_1:
 ...
 break;

case PACKET_TYPE_2:
 ...
 break;
}
따위의 구문이 일반적이다. 그러나, 패킷 종류가 수십 개를 넘어 간다면 위의 형태 처럼 프로그램을 만드는 것은 바람직해 보이지 않는다.

여기서는 2가지 방법을 소개해 보려고 한다.
첫 번째는 함수 포인터를 이용하는 방법이다. 엄밀하게 사용하여야 하지만, 직관적이고 편리하다.
두 번째는 템플릿를 이용하는 방법이다. 첫번째에 비하여 훨씬 유연하지만, 잘못 제작할 경우 런타임 비용이 클 수 있다.

두가지 방법 모두 일단 STL의 map을 활용하자. map 대신, 배열을 사용할 수도 있지만 유연성을 위해서 map을 사용하겠다.

함수 포인터를 이용하는 방법은 아래와 같은 형태를 취한다.
class packet_class
{
};

void packet_function_1(packet_class* packet)
{
}
void packet_function_2(packet_class* packet)
{
}

int _tmain(int argc, _TCHAR* argv[])
{
	typedef void (*FUNCTION)(packet_class* packet);
	std::map<int, FUNCTION> function_map;

	function_map[1] = &packet_function_1;
	function_map[2] = &packet_function_2;

	packet_class* packet;
	function_map[0](packet);
}
이해하기 어려운 내용은 없다.
단지 불편한 점이 있다면, 전역 함수를 사용하기 때문에 객체 지향 위주의 프로그램에서는 객체를 넘기는 방법을 마련해야 한다는 것이 그거다.
그러면 함수 포인터를 멤버 함수 포인터로 바꾸자.

class packet_class
{
};

template<class TYPE>
class dispatch_class
{
public:
	typedef void (TYPE::*DISPATCH_FUNCTION)(packet_class* packet);

protected:
	TYPE* m_dispatch_instance;
	std::map<int, DISPATCH_FUNCTION> m_functions;

public:
	dispatch_class(TYPE* dispatch_instance) : m_dispatch_instance(dispatch_instance) {}
	void set_dispatch(int packet_type, DISPATCH_FUNCTION f)
	{
		m_functions[packet_type] = f;
	}

	void packet_dispatch(int packet_type, packet_class* packet)
	{
		DISPATCH_FUNCTION f = m_functions[packet_type];
		if (f != NULL)
		{
			(m_dispatch_instance->*f)(packet);
		}
	}
};

class dispatcher
{
public:
	void packet_function_1(packet_class* packet)
	{
	}
	void packet_function_2(packet_class* packet)
	{
	}

public:
	dispatcher()
	{
	}
};

int _tmain(int argc, _TCHAR* argv[])
{
	dispatcher packet_dispatcher;
	dispatch_class<dispatcher> dispatcher_executor(&packet_dispatcher);
	dispatcher_executor.set_dispatch(0, &dispatcher::packet_function_1);
	dispatcher_executor.set_dispatch(2, &dispatcher::packet_function_2);

	
	packet_class* packet;
	dispatcher_executor.packet_dispatch(0, packet);
}

앞서 사용한 함수 포인터와 멤버 함수 포인터의 차이점만 알고 있다면 크게 어려운 내용은 아닐 것이다.
멤버 함수 포인터 변수를 선언하는 방법과 그 변수형으로 저장된 포인터, 클래스 객체를 이용하여 멤버 함수를 호출하는 방법만 눈여겨 보기 바란다. 이 내용은 이해하기 보단 그냥 그대로 외워야 한다.

이제 템플릿을 이용해 보자. 그리고 펑크터도 이용할 것이다.
[C/C++ , 좋은 프로그래밍 - 재사용 : 템플릿 - 1]에서 미리 언급하였지만, 펑크터는 클래스 객체의 operator()(type)를 활용하여 객체를 마치 전역 함수처럼 호출하는 방법이다.
사실 멤버 함수 호출하는 방법을 굳이 operator()(type)을 쓰는 것이 꼭 필요하겠냐는 의문이 생긴다.
그러나 아래 예제를 보면 조금 이해가 쉬울 것이다. 펑크터의 장점은 함수 포인터와 여러 클래스 사이의 형의 차이를 알 필요가 없다는 점이다.


void static_function()
{
	printf("11");
}

struct functor
{
	void operator()()
	{
		printf("22");
	}
};

template<class F> 
void caller(F f)
{
	f();
}

int _tmain(int argc, _TCHAR* argv[])
{
	caller(&static_function); //case 1

	functor f;  //case 2
	caller(f);
}
즉, 값을 받아 들이는 입장에서는 객체의 특정 멤버 함수를 알 필요도 없고, 값이 객체인지 함수 포인터인지 알 필요가 없다. 주의 할 점이 이 이야기는 런타임에 관련된 이야기가 아니다. 컴파일 중, caller가 f를 어떻게 호출해야 할지 자동으로 결정한다는 이야기이다.

이제 앞선 dispatch_class를 약간 수정하자.

template<class F>
class dispatch_class
{
protected:
	std::map<int, F> m_functions;

public:
	dispatch_class(){}
	void set_dispatch(int packet_type, F f)
	{
		m_functions[packet_type] = f;
	}

	void packet_dispatch(int packet_type, packet_class* packet)
	{
		auto p = m_functions.find(packet_type);
		if (p != m_functions.end())
		{
			(p->second)(packet);
		}
	}
};

그럴 듯 보이는가? 사실 이 클래스를 사용하기에는 큰 문제점이 있다. class F가 클래스의 템플릿 파라미터라는 것이 문제다. 위 모양을 보면 템플릿 클래스가 컴파일러에 의해서 전개되기 전에 F 형이 반드시 결정되어 있어야 한다. 패킷 디스패치 기능을 만들어 내기 위해서는 다양한 함수를 등록할 수 있어야 하지만 이 템플릿은 클래스 하나당 함수 하나를 등록할 수 있기 때문에 목적에 맞지 않다. 방법이 없을까? 아니다. 이제 가장 중요한 템플릿 3개를 소개하겠다.

std::function (vs2008일 경우, std::tr1::function)
std::bind (vs2008일 경우, std::tr1::bind)
std::placeholders::_1, _2, ... (vs2008일 경우 std::tr1::placeholders::_1, _2,....)

function은 모든 호출 가능한 형을 대신할 수 있다.
bind는 모든 호출 가능한 형과 그 파라미터를 조합할 수 있다.
placeholders는 모든 알려지지 않은 호출 파라미터를 대표할 수 있다.

위에 소개한 템플릿의 유용성은 이루 말할 수 없을 정도이다. 꼭 사용법을 숙지해두길 권장한다.
위의 템플릿을 실제로 구현하기 위해서는 템플릿 리스트 기법을 사용하던가 아니면 c++11의 Variadic template을 사용해야 한다. 여기서는 템플릿의 사용 방법을 이야기 하고, 추후 간단히 구현 원리를 소개하겠다.



이 3가지 조합으로 모든 호출을 단일 펑크터로 만들 수 있다. 다시 dispatch_class를 수정해 보자.

class dispatch_class2
{
protected:
	std::map<int, std::function<void(packet_class*)>> m_functions;

public:
	dispatch_class2(){}
	void set_dispatch(int packet_type, std::function<void(packet_class*)> f)
	{
		m_functions[packet_type] = f;
	}

	void packet_dispatch(int packet_type, packet_class* packet)
	{
		std::function<void(packet_class*)> f = m_functions[packet_type];
		if (f)
		{
			f(packet);
		}
	}
};

이 클래스와 앞서 소개했던 함수 포인터 형태와의 차이점을 이해 하겠는가?
일견 비슷해 보이지만, 함수 포인터 형태는 그 형이 정해져 있다. 전역 함수이던, 멤버 함수이던 호출 파라미터를 비롯한 형에 관련된 정보를 개발자가 미리 맞춰 주어야 한다. function 사용 클래스는 유연하다. 다음 예제를 보자

class dispatch_test
{
public:
	void packet_1(packet_class* packet)
	{
	}
	void packet_2(packet_class* packet)
	{
	}
};

void d1(packet_class* packet)
{
}

void d2(dispatch_test* dt, packet_class* packet)
{
}

struct my_param {};

class dispatch_test2
{
public:
	void packet_1(my_param* p, packet_class* packet)
	{
	}
	void packet_2(packet_class* packet, int p2)
	{
	}
};

int _tmain(int argc, _TCHAR* argv[])
{
	dispatch_test instance;
	dispatch_test2 instance2;
	my_param mp;

	dispatch_class2 test2;
	test2.set_dispatch(0, std::bind(&dispatch_test2::packet_1, &instance2, &mp, std::placeholders::_1));
	// instance2의 멤버 함수를 등록하고, 추가로 mp의 포인터가 첫번째 파라미터로 전달되게 한다. 
	// dispatch_class에서 입력하는 packet_class* 는 두번째 파라미터로 전달한다.

	test2.set_dispatch(1, std::bind(&dispatch_test2::packet_2, &instance2, std::placeholders::_1, 100));
	//instance2의 멤버 함수를 등록하고, 추가로 두번째 파라미터로 int를 전달한다.
	
	test2.set_dispatch(3, std::bind(d1, std::placeholders::_1));
	//전역함수 d1을 등록한다.
	
	test2.set_dispatch(4, std::bind(d2, &instance, std::placeholders::_1));
	//전역 함수 d2를 등록하고 첫번째 파라미터로 instance를 사용하도록 한다.
	
	return 0;
}
dispatch_class와 비교하여 dispatch_class2는 함수 형에 거의 제한이 없다. bind 함수의 인자들은 bind 함수로 전달 되는 인자들은 bind로 만들어지는 클래스의 멤버로 어딘가에 저장되어 있다. 어딘가에 저장된 멤버들이 실제 함수로 넘겨질때, 비효율적으로 사용되지 않도록 주의해서 설계해야 할 것이다.








댓글 없음: