요즘 하고 있는 일 중에 굉장히 제한적으로 사용되는 closure를 구현할 일이 생겼다 — 물론 C++ 기반이다. Python기반이면 이런 걱정은 안해도. Orz
Closure를 정의하자면, (from wikiepdia)
In computer science, a closure is a function that is evaluated in an environment containing one or more bound variables.
이런 애가 되는데, 좀 더 내가 필요로하는 응용에서 생각하면(내 멋대로), 대략 함수와 이를 호출하기 위한 데이터(특히나 lexical 정보들)이 될 것이다. (물론 실제 컴퓨터공학/전산 과학적인 의미는 이와 차이가 있다)
개략적으로 그려보면 이렇게 생겨먹은 Closure 객체 인스턴스를 만들어낼 수 있으면 된다. Execute가 호출되면 Closure에 담겨있던 환경에 따라 "특정 함수"가 실행되는 그런 객체.
class Closure abstract /// abstract base for closure implemenataion { public: Closure() { } virtual ~Closure() { } virtual void Execute() = 0; };
물론 이렇게 기반 클래스를 만든 것은 C++ 타입 시스템 때문이다[…].
실제로 필요한 일을 표현하자면,
- Persistent한 – 최소한 특정 closure가 사용되는 동안은 – 객체가 있고, 이 객체의 특정 멤버 함수를 호출한다
- 호출 시점은 미리 정의되지 않는다. 다만 객체가 지워지기 전에 언젠가는 호출 될 수 있다
- 객체에 묶여있지 않은 – 멤버변수가 아닌 – 인자들이 다수 존재한다
- Lexical 환경의 변수값들은 쓰지 않는다 (이러면 굉장히 기능없고/제약없는 애가 된다)
대충 이정도다. 사실 C++에서는 함수 포인터를 보관할 편한 방법을 제공하지 않기 때문에, 특히나 멤버 함수 쯤 되면 특정 객체의 타입에 묶인 타입이 되서 더더욱 -_- 사용하기가 괴롭다. 그래서 동원한 방법은 C++의 딱풀(…) boost 라이브러리. boost::function 혹은 boost::functional 이나 boost::mem_fn 이 "함수의 일반적인 타입으로의 변환"을 가능케 해준다.
boost::function 을 사용하면 이런 애가 생겨난다. 인자가 전혀 없고 특정 객체의 특정 멤버를 가지고 실행되는 애라면 이런 모양.
#include <boost/shared_ptr.hpp> #include <boost/function.hpp> using boost::shared_ptr; using boost::function;template< typename T, typename R > class Closure0 : public Closure/// 인자가 없는 멤버함수의 Closure { protected: typedef boost::function<R (T&)> FT; T& m_Obj; FT m_Function; public: Closure0( T& obj, FT func ) : m_Obj(obj), m_Function(func) { } virtual ~Closure0() { } virtual void Execute() override { m_Function( m_Obj ); } };/// Closure0를 생성하는 보조 함수 template< typename T, typename R > inline shared_ptr<Closure> MakeClosure( T& t, R (T::*func)() ) { return shared_ptr<Closure>( new Closure0<T, R>( t, func ) ); }
이런 식이면 대충 MakeClosure( 특정 타입의 인스턴스, &특정 타입 :: 멤버 함수 ) 로 내가 원하는 애가 만들어진다.
물론 요구 사항에서 밝힌 것 처럼 "다수의 인자가 필요하면" 좀 복잡한 모양이 된다(…). 심지어 노가다스러운 내용이…
template< typename T, typename R, typename A1, typename A2, typename A3, typename A4 > class Closure4 : public Closure /// 인자가 4개인 멤버함수의 Closure { protected: typedef function<R (T&, A1, A2, A3, A4)> FT; T& m_Obj; FT m_Function; A1 m_A1; A2 m_A2; A3 m_A3; A4 m_A4; public: Closure4( T& obj, FT func, A1 a1, A2 a2, A3 a3, A4 a4 ) : m_Obj(obj), m_Function(func), m_A1(a1), m_A2(a2), m_A3(a3), m_A4(a4) { } virtual ~Closure4() { } virtual void Execute() override { m_Function( m_Obj, m_A1, m_A2, m_A3, m_A4 ); } }; template< typename T, typename R, typename A1, typename A2, typename A3, typename A4 > inline shared_ptr<Closure> MakeClosure( T& t, R (T::*func)(A1,A2,A3,A4), A1 a1, A2 a2, A3 a3, A4 a4 ) { return shared_ptr<Closure>( new Closure4<T, R, A1, A2, A3, A4>( t, func, a1, a2, a3, a4 ) ); }
…이런 무시무시한(?) 물건이 나온다. 문제는 인자 수에 따라서 이걸 다 만들어줘야한다는 것. 사실 T가 될 수 있는 모든 객체에 특정 함수를 강제할 수 있다면 상관없지만(사실 상의 Command 패턴의 각 개체들 같은 애가…) 그것은 불가능한 상황이고, 특정 객체의 서로 다른 함수가 다 Closure로 바뀔 수 있는 상황. 그래서 이런 식으로 "컴파일 타임 / 실행 시간" 중 둘 중 하나에는 생성해낼 수 있는 코드들이 필요한데 -_-;;
저렇게 노가다 안 하고 편하게 할 방법은 없나 궁굼하다 -_-;;
ps. 덤으로 MakeClosure<blah, blah, blah> 같은 보조 함수를 작성해야하는 C++의 template 인자 추론도 맘에 안들기는(후략)
어라, 궁금한게 m_Obj의 삭제도 신경써야 하는거 아닌가요? 언제 호출될지 모르면.. 살짝 미묘하네요. (int 같은 경우도있고요)
저는 다인자 처리할때 BOOST_PP를 쓰고 아무도 유지보수할수없는 코드를 만들었습니다 >ㅁ< (패킷이랑 디비에서)
일반화된 지연 호출; 인자 개수마다 만들어주는거 끔찍하지. PHP로 코드 제너레이터 만들어서 발랐던 기억이 있는데 이거였는지 딴거였는지 가물가물하다.
lua_tinker같은 것도 인자 개수마다 템플릿을 만들던데 뭐 뾰족한 수가 없지 싶다. BOOST_PP 가 반복되는 부분을 많이 줄여주긴 하지만 기본적으로 C 전처리기 가지고 코드제너레이션을 하는 것이기 때문에 수정하기 매우 까다롭지.
ipkn / 그 부분은 조건에 “일정 시간 동안 persistent”하다고 한게 그 의미였음. 사실 내가 쓰는 거에선 리퍼런스 카운터를 쓰고 + 저기서 protected인 멤버 변수들이 좀 다른 의미로 이용되지;
* T 타입이 정말로 persistent한 객체이거나(주로 싱글턴)
* T 타입의 리퍼런스 카운터를 조절할 방법을 저 Closure를 상속받은 클래스의 생성/소멸에서 컨트롤 하는 방법을 사용하거나
해서 조절되게하지.
Rica / 역시 세상에 왕도는 없는건가;
ipkn / boost::PP를 쓰면 다인자 처리가 가능하긴 하구나 -_-a
근데 이걸 나 말고 누가 유지보수할 수 있을런지는…
Boost::preprocessor 로 템플릿 코드 생성…
어제 일종의 지연된 함수 호출 클래스 구현에 관해서 포스팅을 했다. (제한된 Closure라기보단 이 쪽 의미에 가까운 것 같다) 거기에다가 “뭔가 인자 수에 따라 다 정의해야 하는 것을 피할 …
boost::Bind를 써서…void (void) 형태의 함수객체형태로 변환하고, 이를 생성자 인자로 받도록 하는 방법이 좀 더 편하지않을까요. ^^
kalstein / 그 경우엔 제가 의도한 목적과 부합되지 않습니다. 제가 원한건 객체와 그 멤버함수 그리고 그에 대한 인자들이 있을 때 별다른 신경을 안 쓰고도 나중에 호출할 수 있는 래퍼를 만들려는 것입니다.
boost::Bind를 다 쓰고나서 생성자에 옮기면 라이브러리(?)야 간단해지지만 호출하는 사람은 boost::bind를 사용하는 귀찮음이 있지요.
물론 MakeClosure()에서 말씀하신 방법을 쓸 수 있지만, 생성 시점 / 호출 시점이 다를 수 밖에 없는 이런 방식에서는 나중에 디버깅을 편하게 하려고 인자를 쉽게 볼 수 있는 형태로 분해해두는게 조금 더 편합니다(…)