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

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

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

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

원래 다음 템플릿에 해당하는 걸 썼다.[1] 

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 단계를 지나고 나면 아마 더 많은 컴파일러 들도 지원할 것으로 보인다.[2]  그리고 내가 예전에 생각한 것처럼, 미래(?)가 되어 일이 좀 편해지는 듯 하다.

  1. 실제로는 BOOST_PP가 이걸 생성해 낸다. []
  2. intel cc도 지원하긴 하는데 정확한 버전을 모르겠다. []

Published by

rein

나는 ...

12 thoughts on “C++0x를 써서 Closure 다시 만들기”

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

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

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

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

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

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

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

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

    1. 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 가 나왔다… 이쪽 오버헤드 차이가 더 크네;;

    1. http://social.msdn.microsoft.com/Forums/en/vclanguage/thread/fb56d0df-65ad-4c5c-8b6d-c943563e7d24
      아래 댓글에
      Note that using std::function has significant overhead compared to the template route as it makes use of type erasure and RTTI internally at runtime, whereas the template will be statically resolved at compile time. I.e., if performance matters, don’t use std::function for this.

      라고 되어 있네요 std::function이 RTTI를 내부적으로 사용한다고 하네요.

    1. 넵. 저도 구글리더에 우주괴물님 포스팅과 제 블로그 댓글이 동시에 올라오는 걸 봤습니다(?)

Leave a Reply