C++기반의 closure

요즘 하고 있는 일 중에 굉장히 제한적으로 사용되는 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 인자 추론도 맘에 안들기는(후략)

Jinuk Kim
Jinuk Kim

SW Engineer / gamer / bookworm / atheist / feminist

Articles: 935

8 Comments

  1. 어라, 궁금한게 m_Obj의 삭제도 신경써야 하는거 아닌가요? 언제 호출될지 모르면.. 살짝 미묘하네요. (int 같은 경우도있고요)

    저는 다인자 처리할때 BOOST_PP를 쓰고 아무도 유지보수할수없는 코드를 만들었습니다 >ㅁ< (패킷이랑 디비에서)

  2. 일반화된 지연 호출; 인자 개수마다 만들어주는거 끔찍하지. PHP로 코드 제너레이터 만들어서 발랐던 기억이 있는데 이거였는지 딴거였는지 가물가물하다.

    lua_tinker같은 것도 인자 개수마다 템플릿을 만들던데 뭐 뾰족한 수가 없지 싶다. BOOST_PP 가 반복되는 부분을 많이 줄여주긴 하지만 기본적으로 C 전처리기 가지고 코드제너레이션을 하는 것이기 때문에 수정하기 매우 까다롭지.

  3. ipkn / 그 부분은 조건에 “일정 시간 동안 persistent”하다고 한게 그 의미였음. 사실 내가 쓰는 거에선 리퍼런스 카운터를 쓰고 + 저기서 protected인 멤버 변수들이 좀 다른 의미로 이용되지;

    * T 타입이 정말로 persistent한 객체이거나(주로 싱글턴)
    * T 타입의 리퍼런스 카운터를 조절할 방법을 저 Closure를 상속받은 클래스의 생성/소멸에서 컨트롤 하는 방법을 사용하거나

    해서 조절되게하지.

  4. ipkn / boost::PP를 쓰면 다인자 처리가 가능하긴 하구나 -_-a
    근데 이걸 나 말고 누가 유지보수할 수 있을런지는…

  5. Boost::preprocessor 로 템플릿 코드 생성…

    어제 일종의 지연된 함수 호출 클래스 구현에 관해서 포스팅을 했다. (제한된 Closure라기보단 이 쪽 의미에 가까운 것 같다) 거기에다가 “뭔가 인자 수에 따라 다 정의해야 하는 것을 피할 …

  6. boost::Bind를 써서…void (void) 형태의 함수객체형태로 변환하고, 이를 생성자 인자로 받도록 하는 방법이 좀 더 편하지않을까요. ^^

  7. kalstein / 그 경우엔 제가 의도한 목적과 부합되지 않습니다. 제가 원한건 객체와 그 멤버함수 그리고 그에 대한 인자들이 있을 때 별다른 신경을 안 쓰고도 나중에 호출할 수 있는 래퍼를 만들려는 것입니다.

    boost::Bind를 다 쓰고나서 생성자에 옮기면 라이브러리(?)야 간단해지지만 호출하는 사람은 boost::bind를 사용하는 귀찮음이 있지요.

    물론 MakeClosure()에서 말씀하신 방법을 쓸 수 있지만, 생성 시점 / 호출 시점이 다를 수 밖에 없는 이런 방식에서는 나중에 디버깅을 편하게 하려고 인자를 쉽게 볼 수 있는 형태로 분해해두는게 조금 더 편합니다(…)

Leave a Reply