C++0x를 써서 Closure 다시 만들기

예전에 포스팅 한 것 처럼, 2 년 전, 이맘 때 쯤, boost 라이브러리를 이용해서 C++ 용 Closure 구현체를 만들었다. 이걸 만들 때 겪었던 어려운 점들은 다음과 같다.

  • 함수의 타입을 인식해야 하기 때문에 가변 인자 템플릿을 만들어야 했고, 이를 위해 boost::preprocessor 의존성이 생겼다
  • 인자의 생명 주기를 추정할 수 없기에, 간단한 가정을 하고 모든 참조형 인자(T& 타입)를 강제로 원래 타입으로 지정(T 타입)해서 저장하도록 했다.
  • 함수를 ‘저장’하기 위해 boost::function 을 사용했다. (std::tr1::function 과 동일함)

C++ 0x 에는 lambda function 문법이 있다. 이를 이용해서 위의 세 가지를 간단히(?)해결할 수 있게 되었다.

원래 다음 템플릿에 해당하는 걸 썼다. ((실제로는 BOOST_PP가 이걸 생성해 낸다.)) 

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 );
    }
};

하지만 익명 함수 문법인 lambda function을 사용하면 다음 한 가지 형태로 내가 이전 구현에 생각했던 모든 함수 형을 다룰 수 있다.

template<typename fun>
class LambdaClosure : public Closure
{
    fun toExec;    
public:
    LambdaClosure(fun& f) : toExec(f) {}
    virtual ~LambdaClosure() {}
    virtual void Execute() override sealed { toExec(); }
};

우선 함수의 타입 인식 문제. 어차피 타입 이름을 알 수 없지만(익명 함수니까), 이건 템플릿 함수의 타입 추론이 해결해준다. 즉, 단순히 아래 함수를 만들면 해결.

template<typename fun> 
std::tr1::shared_ptr<Closure> MakeLambdaClosure(fun f) 

    return std::tr1::shared_ptr<Closure>(new LambdaClosure<fun>(f)); 
}

 

함수 자체의 타입이야 있겠지만, 이걸 굳이 boost::function 혹은 std::(tr1::)function 에 넣지 않고도 가볍게(?) 저장할 수 있다.

그리고 인자의 생명 주기 자체는 내가 작성하는 Closure 코드에서 전부 알아서 할 게 아니라, lambda function의 argument capture 문법에서 원하는 데로(?) 참조인지 복사인지 각 인자 별로, 혹은 적당히(?) 지정하면 끝.

마지막으로 추가 라이브러리 사용이 없다는 건 장점. 게다가 lambda function은 선언하는 scope 에서 friend 함수로 간주되기에, 이전에는 어쩔 수 없이 public 으로 노출해야 하던 함수들이 굳이 노출될 필요가 없어진다.

 

그리고 기나긴(?) template instantiation 된 함수 호출들이 단순히(?) 호출 한 번으로 끝나기 때문에 함수 호출 오버헤드도 약간 감소한다. (아주 간단하게 객체 내부 변수를 1씩 더하는걸 이전 구현과 새 구현 기준으로 비교했더니 Core2Duo 6750 에서 597ms vs. 181ms 정도의 차이가 있었다; 실행 횟수는 8천만번 호출하는걸 256회 반복해서 낸 평균; 그러니까 어느 쪽도 충분히 큰 오버헤드는 아니다.)

덤으로, 예전대로라면 별도의 함수를 작성해야 하는데, 그 부분이 없어지고, 실제로 의미를 따라가기 쉽게(혹은 어렵게?) “Closure를 생성하는 위치”에 함수 본문(body)이 온다는 것도 꽤 편하다…

 

일단 MS VisualStudio 2010 과 GCC 4.5 에는 이 익명함수가 존재하고 있고, draft 단계를 지나고 나면 아마 더 많은 컴파일러 들도 지원할 것으로 보인다. ((intel cc도 지원하긴 하는데 정확한 버전을 모르겠다.))  그리고 내가 예전에 생각한 것처럼, 미래(?)가 되어 일이 좀 편해지는 듯 하다.

Jinuk Kim
Jinuk Kim

SW Engineer / gamer / bookworm / atheist / feminist

Articles: 935

12 Comments

  1. 안녕하세요. RSS 구독자 입니다. 매번 좋은 글 잘 읽고 있습니다.
    다름이 아니오라 지금 말씀하신 방식이 C++0x 와 어떤 연관이 있는지 궁금해서 질문 드립니다.

    • 이전에 지연된 함수 호출을 하기 위해 — Java executor service 혹은 Future 비슷한 것 — C++ 기반의 closure 클래스를 작성했습니다. 이게 예전 구현체는 boost::function (혹은 tr1에 추가된 같은 형태의 function) 템플릿을 사용해서 만들었던 물건인데, C++ 0x에 추가된 lambda 함수 문법을 이용하면 “더 효율적이고, 더 사용하기 편하고, 더 알아보기 쉽게” 다시 작성할 수 있다는 의미입니다.

      위 코드에 실제 lambda 함수 구현체가 없어서 이게 눈에 안보이는데, 이건 곧(?)글로 쓰도록 하겠습니다.

      해당 글을 작성했습니다. 참고하세요 ~_~

  2. 사실 람다 자체는 기존 C++의 함수 객체에도 1:1로 매핑이 되는 내용이지만 언어 자체에서 이렇게 지원해준다는게 표현력에 있어서 정말 큰 차이를 가져오는 것 같습니다. 예전에 런타임에서도 좀 제네릭한 코드 하나 쓰려면 얼마나 삽질을 했어야 하는지 (…)

    • C++ 함수 객체 만들려고 써야하는 그 귀찮은 문법들을 생각하면 lambda 함수의 편의성은 정말 기쁨의 눈믈을 쏟아낼 지경이죠 ㅠㅠ

  3. 밑 글에 이어, C++ 0x lambda를 사용한 closure 예제…

    바로 전 글에 이어, 두 개의 예제 차이(?)를 들어보겠다. class SomeObject  {   int m_value; public:   SomeObject() : m_value(0) {}   int Get() const { return m_value; }   void…

  4. auto와 lambda가 컴파일러 레벨의 기본 기능으로 들어가서 정말 감격이죠. 예전에도 boost를 써서 못할 건 없었지만, 다른 팀원을 설득하기가;;;

    그런데, 전에 C++ 0x draft 진행 관련해서 herb shutter가 쓴 글 보니까, 원래 lambda의 경우, boost::lambda 처럼 라이브러리 형태로 구현할지, 컴파일러 레벨에서 처리할지에 대해 논의가 꽤 있었다고 하더군요. boost::lambda 처럼 나왔으면, 결국 팀원들과 사용하진 못했을 듯. -0-

    • auto, lambda, decltype 이 표준 라이브러리가 아니라 언어 명세에 포함된건 정말 쌍수를 들고 환영할 일이죠 :D

      boost::function 써서 만든 코드가 있는데(…) 이걸 팀에서도 쓴 이유가 “기존 코드로 하면 너무 길어서”였고, 알아야하는게 템플릿 함수 하나라서 겨우 가능했던걸 생각하면, 라이브러리 형태라면 정말 포기했을꺼에요(…).

  5. 똑같은 테스트를 32bit linux / gcc-4.5.1(prerelease) / xeon 3060(2.4Ghz) 머신에서 돌렸더니 대략
    std::function vs. lambda function => 1627ms vs. 314 ms 가 나왔다… 이쪽 오버헤드 차이가 더 크네;;

Leave a Reply