[C++] 클로져(Closures)가 그래서 무엇인가요
Effective Modern C++ 책에서 Lambdas (람다) 섹션쪽을 읽다 보면 Closures (클로져) 얘기가 많이 나온다. C++ 에서 클로져가 정확히 뭘까. 어떤 이는 람다가 클로져라고 하기도 하고, 어떤 이는 캡쳐와 클로져를 같이 설명하면서 더 이해하기가 어려워지기도 한다. 검색을 이리저리 해보다가 이펙티브 책 시리즈의 저자 Scott Meyers 의 블로그에서 람다와 클로져에 대해 설명한 글을 보았다. 아마도 나같이 혼란스러워 하는 사람들이 많았었는지 주변에서 많이들 질문했었나보다.
그의 블로그 글 Lambdas vs. Closures 에서 짤막하지만 C++ 의 대가 답게 람다와 클로져의 차이점에 대해 정확하게 설명해 놓았다. 아래 간단히 번역을 해보았다.
” In recent days, I’ve twice found myself explaining the difference between lambdas and closures in C++11, so I figured it was time to write it up.
최근, 람다와 클로져에 대한 차이점에 대해 두번이나 설명하고 있는 자신을 발견하고 이 글을 쓰기로 했다.
The term “lambda” is short for lambda expression, and a lambda is just that: an expression. As such, it exists only in a program’s source code. A lambda does not exist at runtime.
“람다”라는 것은 람다 표현식의 준말이고, 그저 표현식일 뿐이다. 그것은 단지 프로그램의 소스 코드에서만 존재한다. 런타임에서는 람다는 존재하지 않는다.
The runtime effect of a lambda expression is the generation of an object. Such objects are known as closures.
람다 표현식에 대한 런타임 결과는 오브젝트의 생성이다. 그러한 오브젝트를 클로져라고 한다.
Given
아래 예를 보면,
auto f = [&](int x, int y) { return fudgeFactor * (x + y); };
the blue expression to the right of the “=” is the lambda expression (i.e., “the lambda”), and the runtime object created by that expression is the closure.
“=” 오른쪽에 있는 표현식이 람다이고, 이 표현식으로 부터 생성된 런타임 오브젝트가 클로져이다.
You could be forgiven for thinking that, in this example, f was the closure, but it’s not. f is a copy of the closure.
위의 예에서 f를 클로져라고 생각할 수도 있지만, 그건 아니다. f는 클로져의 복사본이다.
The process of copying the closure into f may be optimized into a move (whether it is depends on the types captured by the lambda), but that doesn’t change the fact that f itself is not the closure.
클로져를 복사하는 과정은 move로 최적화될 수 있지만 f가 클로져가 아니라는 사실은 변하지 않는다.
The actual closure object is a temporary that’s typically destroyed at the end of the statement.
실제 클로져 오브젝트는 임시 객제로 그 줄의 끝에서 파괴된다.
The distinction between a lambda and the corresponding closure is precisely equivalent to the distinction between a class and an instance of the class.
람다와 클로져의 차이는 정확하게 클래스와 클래스 인스턴스의 차이와 동일하다.
A class exists only in source code; it doesn’t exist at runtime.
클래스는 오직 소스코드에서만 존재하고, 런타임에서는 존재하지 않는다.
What exists at runtime are objects of the class type.
런타임에서 존재하는 것은 클래스 타입의 오브젝트들이다.
Closures are to lambdas as objects are to classes.
클로져와 람다의 관계는 오브젝트와 클래스의 관계와 같다.
This should not be a surprise, because each lambda expression causes a unique class to be generated (during compilation) and also causes an object of that class type–a closure–to be created (at runtime).”Lambdas vs. Closures from SCOTT MEYERS
놀라울게 없는게, 각 람다 표현식은 컴파일 과정에서 고유한 클래스를 만들어내고, 그 클래스 타입의 오브젝트(클로져)가 (런타임에서) 생성된다.
간단히 정리해보면, 클로져는 런타임에서만 존재하는 객체이고, 람다와의 관계는 클래스(람다)를 정의하고 클래스의 객체(클로져)를 만드는 것과 동일하다고 보면 될 것 같다.
여기서 문득 드는 생각은, 클로져라는 것이 컴파일러에서 임시 클래스를 만들어주고, 객체를 생성해주는 일을 한다면 클로져란 개념을 반드시 알아야 하는 것인가? 여기에 대한 답은 Effective Modern C++ 책의 람다 섹션 (챕터 6) 에서 찾을 수 있었다.
책에 언급된 예제 중에 item 31에 있는 것 하나만 가져와서 살펴보면,
void addDivisorFilter()
{
auto calc1 = computeSomeValue1();
auto calc2 = computeSomeValue2();
auto divisor = computeDivisor(calc1, calc2);
filters.emplace_back( // danger!
[&](int value) { return value % divisor == 0; } // ref to
); // divisor
}
//Code Sample in item 31, Page 217, Effective Modern C++, Scott Meyers
[&] 를 사용하여 로컬 변수들을 레퍼런스 캡쳐(갈무리)하였고, 람다식 [&](int value) { return value % divisor == 0; }
에서는 로컬 변수 divisor를 참조하고 있다. 즉, 클로져(컴파일러가 생성하는 런타임 객체)가 로컬 변수 divisor의 레퍼런스를 가지게 되는 경우이다. filters라는 컨테이너에 클로져가 보관되기 때문에 여기서 클로져의 수명은 로컬 변수의 수명보다 오래 지속되고, 클로져가 레퍼런스로 가지고 있는 divisor는 댕글링될 수 밖에 없다. 여기서 클로져의 개념을 이해하지 못한다면 위의 예제를 이해하기가 어렵게 되고, 그러한 프로그램도 undefined behaviour 를 야기할 수 있다.
이러한 것을 방지하기 위한 방법은 이펙티브 모던 C++ 책에서 여러가지로 언급하고 있으니 한번 읽어보기를 추천한다.
우연히 왔다가 좋은 블로그 찾고 갑니다.
감사합니다 🙂
람다공부하다가 클러져가 나와서 와 뭐고 했는데 좋은 글 읽고 갑니다
감사합니다. 🙂