rein's world

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에 담겨있던 환경에 따라 “특정 함수"가 실행되는 그런 객체.

/// abstract base for closure implemenataion
class Closure abstract {
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;

/// 인자가 없는 멤버함수의 Closure
template<typename T, typename R>
class Closure0 : public 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(특정 타입의 인스턴스, &특정 타입::멤버 함수) 로 내가 원하는 객체가 만들어진다.

물론 요구 사항에서 밝힌 것 처럼 “다수의 인자가 필요하면” 좀 복잡한 모양이 된다. 심지어 노가다스러운 내용이…

/// 인자가 4개인 멤버함수의 Closure
template< typename T, typename R,
    typename A1, typename A2, typename A3, typename A4 >
class Closure4 : public 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 인자 추론도 맘에 안들기는 마찬가지.