예전에 포스팅 한 것 처럼, 2 년 전, 이맘 때 쯤, boost 라이브러리를 이용해서 C++ 용 Closure 구현체를 만들었다. 이걸 만들 때 겪었던 어려운 점들은 다음과 같다.
- 함수의 타입을 인식해야 하기 때문에 가변 인자 템플릿을 만들어야 했고, 이를 위해 boost::preprocessor 의존성이 생겼다
- 인자의 생명 주기를 추정할 수 없기에, 간단한 가정을 하고 모든 참조형 인자(T& 타입)를 강제로 원래 타입으로 지정(T 타입)해서 저장하도록 했다.
- 함수를 ‘저장’하기 위해 boost::function 을 사용했다. (std::tr1::function 과 동일함)
C++ 0x 에는 lambda function 문법이 있다. 이를 이용해서 위의 세 가지를 간단히(?)해결할 수 있게 되었다.
원래 다음 템플릿에 해당하는 걸 썼다.1
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을 사용하면 다음 한 가지 형태로 내가 이전 구현에 생각했던 모든 함수 형을 다룰 수 있다.
class LambdaClosure : public Closure
{
fun toExec;
public:
LambdaClosure(fun& f) : toExec(f) {}
virtual ~LambdaClosure() {}
virtual void Execute() override sealed { toExec(); }
};
우선 함수의 타입 인식 문제. 어차피 타입 이름을 알 수 없지만(익명 함수니까), 이건 템플릿 함수의 타입 추론이 해결해준다. 즉, 단순히 아래 함수를 만들면 해결.
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 그리고 내가 예전에 생각한 것처럼, 미래(?)가 되어 일이 좀 편해지는 듯 하다.

안녕하세요. RSS 구독자 입니다. 매번 좋은 글 잘 읽고 있습니다.
다름이 아니오라 지금 말씀하신 방식이 C++0x 와 어떤 연관이 있는지 궁금해서 질문 드립니다.
이전에 지연된 함수 호출을 하기 위해 — Java executor service 혹은 Future 비슷한 것 — C++ 기반의 closure 클래스를 작성했습니다. 이게 예전 구현체는 boost::function (혹은 tr1에 추가된 같은 형태의 function) 템플릿을 사용해서 만들었던 물건인데, C++ 0x에 추가된 lambda 함수 문법을 이용하면 “더 효율적이고, 더 사용하기 편하고, 더 알아보기 쉽게” 다시 작성할 수 있다는 의미입니다.
위 코드에 실제 lambda 함수 구현체가 없어서 이게 눈에 안보이는데,
이건 곧(?)글로 쓰도록 하겠습니다.해당 글을 작성했습니다. 참고하세요 ~_~
사실 람다 자체는 기존 C++의 함수 객체에도 1:1로 매핑이 되는 내용이지만 언어 자체에서 이렇게 지원해준다는게 표현력에 있어서 정말 큰 차이를 가져오는 것 같습니다. 예전에 런타임에서도 좀 제네릭한 코드 하나 쓰려면 얼마나 삽질을 했어야 하는지 (…)
C++ 함수 객체 만들려고 써야하는 그 귀찮은 문법들을 생각하면 lambda 함수의 편의성은 정말 기쁨의 눈믈을 쏟아낼 지경이죠 ㅠㅠ
auto와 lambda가 컴파일러 레벨의 기본 기능으로 들어가서 정말 감격이죠. 예전에도 boost를 써서 못할 건 없었지만, 다른 팀원을 설득하기가;;;
그런데, 전에 C++ 0x draft 진행 관련해서 herb shutter가 쓴 글 보니까, 원래 lambda의 경우, boost::lambda 처럼 라이브러리 형태로 구현할지, 컴파일러 레벨에서 처리할지에 대해 논의가 꽤 있었다고 하더군요. boost::lambda 처럼 나왔으면, 결국 팀원들과 사용하진 못했을 듯. -0-
auto, lambda, decltype 이 표준 라이브러리가 아니라 언어 명세에 포함된건 정말 쌍수를 들고 환영할 일이죠
boost::function 써서 만든 코드가 있는데(…) 이걸 팀에서도 쓴 이유가 “기존 코드로 하면 너무 길어서”였고, 알아야하는게 템플릿 함수 하나라서 겨우 가능했던걸 생각하면, 라이브러리 형태라면 정말 포기했을꺼에요(…).
똑같은 테스트를 32bit linux / gcc-4.5.1(prerelease) / xeon 3060(2.4Ghz) 머신에서 돌렸더니 대략
std::function vs. lambda function => 1627ms vs. 314 ms 가 나왔다… 이쪽 오버헤드 차이가 더 크네;;