람다 표현식
캡처·매개변수·반환 타입으로 람다를 구성하고 지역 상태와 STL 알고리즘을 연결하는 패턴 및 주의점을 익힙니다.
이번 장에서는 람다 표현식(Lambda Expressions)의 문법, 캡처 방식, STL 알고리즘에서의 사용 패턴을 다룹니다.
람다 표현식은 C++11에서 도입되었으며, 코드 내에서 짧은 함수를 즉석에서 정의하고 사용할 수 있게 해줍니다.
이는 특히 STL 알고리즘과 함께 사용할 때 코드의 가독성과 유연성을 높입니다.
12장 4절에서 함수 객체(Functors)를 잠깐 다루면서 람다를 언급했지만, 여기서는 람다의 모든 면모를 깊이 있게 탐구할 것입니다.
람다 표현식이란 무엇인가?
람다 표현식은 이름이 없는(익명) 함수 객체(Functor)를 코드 내에서 즉석으로 생성하는 방법입니다.
즉, 별도의 클래스를 정의하거나 전역 함수를 만들 필요 없이, 필요한 곳에 바로 함수처럼 호출할 수 있는 객체를 만들 수 있습니다.
등장 배경- STL 알고리즘(
std::sort,std::for_each,std::transform,std::find_if등)은 사용자 정의 동작을 위해 함수 포인터나 함수 객체를 인자로 받습니다. - 함수 포인터는 상태(멤버 변수)를 가질 수 없고, 함수 객체는 작은 함수를 위해 별도의 클래스를 정의해야 하는 번거로움이 있었습니다.
- 람다 표현식은 이러한 불편함을 해소하고, 인라인으로 함수 객체를 생성할 수 있게 하여 코드를 훨씬 간결하고 의미 있게 만듭니다.
[캡처](매개변수) -> 반환타입 {
// 함수 본문
}[캡처](Capture Clause): 람다 본문 내에서 람다 외부의 변수를 사용할 수 있도록 지정하는 부분입니다. 람다의 핵심 기능 중 하나입니다.(매개변수)(Parameter List): 일반 함수처럼 람다가 받을 매개변수 목록입니다.-> 반환타입(Return Type): 람다의 반환 타입입니다. 보통 생략할 수 있으며, 컴파일러가 본문에서 추론합니다. 반환 타입이 복잡하거나 명시적으로 지정하고 싶을 때 사용합니다.{ 함수 본문 }(Function Body): 람다가 실행할 코드 블록입니다.
캡처 절 (Capture Clause) 상세 분석
캡처 절은 값 복사, 원본 참조, 객체 수명 분리를 기준으로 읽으면 각 표기법의 차이가 선명해집니다.
캡처 절은 람다 표현식이 외부 스코프의 변수를 어떻게 사용할 것인지 결정합니다.
[] (아무것도 캡처하지 않음)- 람다 내부에서 캡처가 필요한 지역 변수를 사용할 수 없습니다. 전역·static 변수에는 접근할 수 있으므로 순수 함수를 보장하지 않습니다.
#include <iostream>
int main() {
int x = 10;
auto func = [](int a, int b) {
// std::cout << x; // 컴파일 오류: x를 캡처하지 않음
return a + b;
};
std::cout << func(1, 2) << std::endl; // 출력: 3
return 0;
}[var] (값으로 캡처 - by value)- 외부 변수
var의 값을 람다 객체 생성 시 복사합니다. 람다 본문 내에서var의 값을 변경해도 외부의 원본var에는 영향을 주지 않습니다. 기본 호출 연산자가 const이므로 값 캡처 멤버를 그 호출 안에서 변경할 수 없습니다.
#include <iostream>
int main() {
int x = 10;
auto func = [x](int a) {
// x++; // 컴파일 오류: 값으로 캡처된 변수는 기본적으로 const
return a + x;
};
x = 20; // 람다 내부의 x는 영향을 받지 않음
std::cout << func(5) << std::endl; // 출력: 15 (5 + 10)
return 0;
}mutable키워드: 값으로 캡처된 변수를 람다 내부에서 수정하고 싶다면, 매개변수 목록 뒤에mutable키워드를 붙여야 합니다.
#include <iostream>
int main() {
int x = 10;
auto func = [x](int a) mutable { // mutable 키워드 추가
x++; // 이제 람다 내부의 x는 수정 가능 (원본 x와 별개)
return a + x;
};
std::cout << func(5) << std::endl; // 출력: 16 (5 + 11)
std::cout << x << std::endl; // 출력: 10 (원본 x는 변함 없음)
return 0;
}[&var] (참조로 캡처 - by reference)- 외부 변수
var를 참조로 캡처합니다. 람다 본문 내에서var를 변경하면 외부의 원본var도 변경됩니다.
#include <iostream>
int main() {
int x = 10;
auto func = [&x](int a) { // x를 참조로 캡처
x++; // 원본 x를 변경
return a + x;
};
std::cout << func(5) << std::endl; // 출력: 16 (5 + 11)
std::cout << x << std::endl; // 출력: 11 (원본 x가 변경됨)
return 0;
}[=] (모든 변수를 값으로 캡처)- 람다 본문 내에서 사용되는 모든 외부 변수를 값으로 캡처합니다. (예외:
this포인터 값을 보관하므로 원본 객체 수명은 연장되지 않습니다. C++20부터 [=]의 암시적 this 캡처는 권장되지 않습니다.)
#include <iostream>
int main() {
int x = 10;
int y = 20;
auto func = [=](int a) { // x, y 모두 값으로 캡처
return a + x + y;
};
std::cout << func(5) << std::endl; // 출력: 35
return 0;
}[&] (모든 변수를 참조로 캡처)- 람다 본문 내에서 사용되는 모든 외부 변수를 참조로 캡처합니다.
#include <iostream>
int main() {
int x = 10;
int y = 20;
auto func = [&](int a) { // x, y 모두 참조로 캡처
x++;
y--;
return a + x + y;
};
std::cout << func(5) << std::endl; // 출력: 35 (a=5, x=11, y=19)
std::cout << "x: " << x << ", y: " << y << std::endl; // 출력: x: 11, y: 19
return 0;
}혼합 캡처: 특정 변수는 값으로, 다른 변수는 참조로 캡처할 수 있습니다.
#include <iostream>
int main() {
int x = 10;
int y = 20;
auto func = [x, &y](int a) { // x는 값으로, y는 참조로
// x++; // 오류: x는 const로 캡처됨
y++; // y는 수정 가능
return a + x + y;
};
std::cout << func(5) << std::endl; // 출력: 36 (a=5, x=10, y=21)
std::cout << "x: " << x << ", y: " << y << std::endl; // 출력: x: 10, y: 21
return 0;
}[this] (C++11) / *this (C++17): 멤버 함수 내에서 람다를 정의할 때, 람다 내부에서 클래스의 멤버 변수나 멤버 함수에 접근하려면 this 포인터를 캡처해야 합니다.
[this]:this포인터 값을 보관하며 원본 객체를 복사하거나 수명을 연장하지 않습니다.[*this](C++17): 현재 객체(*this)를 값으로 복사하여 캡처합니다. 복사본이므로 원본 객체와 람다의 생명 주기를 분리할 때 유용합니다.
람다 표현식의 활용 사례
람다 표현식은 주로 STL 알고리즘과 함께 사용될 때 그 진가를 발휘합니다.
STL 알고리즘은 closure를 반복 호출해 결과를 만든다
find_if가 10·25·30에서 false, 45에서 true 뒤 종료하며 50은 검사하지 않는 구체 실행 추적이다.
C++ · LAMBDA ALGORITHM
find_if는 closure를 호출해 첫 일치 위치를 찾는다
알고리즘은 순회 구조를, closure는 capture한 상태와 현재 원소에 대한 판단을 담당한다.
보조 설명 · 코드와 수명 조건
- 호출 지점
범위와 lambda closure를 알고리즘에 넘긴다.
- 순회
알고리즘이 현재 원소로 closure를 호출한다.
- 판단
closure가 capture와 원소를 이용해 bool을 돌려준다.
- 결과
첫 일치 위치를 반환하며 없으면 end를 반환한다.
본문 {10, 25, 30, 45, 50}에서 n > 30을 검사하면 첫 일치는 45이다. 순회와 판단은 알고리즘 실행 범위 안에서 이루어진다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | 10,25,30을 순서대로 검사해 false를 얻는 반복 뒤 45에서 true를 얻고 그 원소 반복자를 호출자에게 반환한다. 50은 검사하지 않는다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 거짓 후보들 find_if가 앞의 요소부터 조건을 검사한다. |
|
|
| 첫 참 45에서 조건이 처음 성립한다. |
|
|
| 반환과 미검사 45를 가리키는 반복자를 반환한다. |
|
|
STL 알고리즘은 closure를 반복 호출해 결과를 만든다
find_if가 10·25·30에서 false, 45에서 true 뒤 종료하며 50은 검사하지 않는 구체 실행 추적이다.
C++ · LAMBDA ALGORITHM
find_if는 closure를 호출해 첫 일치 위치를 찾는다
알고리즘은 순회 구조를, closure는 capture한 상태와 현재 원소에 대한 판단을 담당한다.
보조 설명 · 코드와 수명 조건
- 호출 지점
범위와 lambda closure를 알고리즘에 넘긴다.
- 순회
알고리즘이 현재 원소로 closure를 호출한다.
- 판단
closure가 capture와 원소를 이용해 bool을 돌려준다.
- 결과
첫 일치 위치를 반환하며 없으면 end를 반환한다.
본문 {10, 25, 30, 45, 50}에서 n > 30을 검사하면 첫 일치는 45이다. 순회와 판단은 알고리즘 실행 범위 안에서 이루어진다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | 10,25,30을 순서대로 검사해 false를 얻는 반복 뒤 45에서 true를 얻고 그 원소 반복자를 호출자에게 반환한다. 50은 검사하지 않는다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 거짓 후보들 find_if가 앞의 요소부터 조건을 검사한다. |
|
|
| 첫 참 45에서 조건이 처음 성립한다. |
|
|
| 반환과 미검사 45를 가리키는 반복자를 반환한다. |
|
|
#include <iostream>
#include <vector>
#include <algorithm> // sort
#include <string>
struct Person {
std::string name;
int age;
};
int main() {
std::vector<Person> people = {
{"Alice", 30},
{"Charlie", 25},
{"Bob", 35}
};
// 나이 기준 오름차순 정렬 (람다 사용)
std::sort(people.begin(), people.end(), [](const Person& p1, const Person& p2) {
return p1.age < p2.age;
});
std::cout << "Sorted by age:\n";
for (const auto& p : people) {
std::cout << p.name << " (" << p.age << ")\n";
}
// 출력:
// Charlie (25)
// Alice (30)
// Bob (35)
return 0;
}#include <iostream>
#include <vector>
#include <algorithm> // find_if
int main() {
std::vector<int> numbers = {10, 25, 30, 45, 50};
// 30보다 큰 첫 번째 요소 찾기
auto it = std::find_if(numbers.begin(), numbers.end(), [](int n) {
return n > 30;
});
if (it != numbers.end()) {
std::cout << "First number greater than 30: " << *it << std::endl; // 출력: 45
} else {
std::cout << "No number greater than 30 found.\n";
}
return 0;
}#include <iostream>
#include <vector>
#include <algorithm> // count_if
int main() {
std::vector<int> scores = {85, 92, 78, 65, 95, 80};
int min_score = 90; // 외부 변수
// min_score 이상인 점수의 개수 세기
int count_above_min = std::count_if(scores.begin(), scores.end(), [min_score](int s) {
return s >= min_score;
});
std::cout << "Number of scores >= " << min_score << ": " << count_above_min << std::endl; // 출력: 2 (92, 95)
return 0;
}람다 표현식의 장점
- 간결성: 작은 함수 객체를 즉석에서 정의할 수 있어 코드의 양이 줄어듭니다.
- 가독성: 함수 정의가 사용되는 곳 바로 옆에 위치하므로, 코드의 의도를 파악하기 쉽습니다.
- 유연성: 외부 스코프의 변수를 캡처할 수 있어, 더 복잡하고 유연한 조건을 표현할 수 있습니다.
- 성능: 컴파일러가 람다를 인라인 처리하거나 최적화하기 더 쉬워, 함수 포인터보다 성능 이점을 가질 수 있습니다.
auto와 람다의 조합
auto 키워드는 람다 표현식의 고유 타입을 간결하게 유지하는 데 사용됩니다.
람다의 실제 타입은 컴파일러가 생성하는 고유한 이름 없는 타입이므로, auto로 고유 타입을 유지해 저장할 수 있습니다. 람다를 직접 함수 인자로 넘기거나 std::function으로 감싸는 방법도 가능합니다.
#include <iostream>
#include <functional> // std::function (선택적)
int main() {
auto my_lambda = [](int a, int b) { return a * b; }; // auto로 람다 타입 추론
std::cout << my_lambda(6, 7) << std::endl; // 출력: 42
// (참고) C++11에서는 람다를 std::function 객체로 감쌀 수 있습니다.
// 타입 소거를 통해 지정한 호출 시그니처로 보관합니다. 함수 포인터와 같은 타입은 아닙니다.
std::function<int(int, int)> func_wrapper = [](int a, int b) { return a / b; };
std::cout << func_wrapper(10, 2) << std::endl; // 출력: 5
return 0;
}람다의 한계 및 주의점
- 복잡성: 람다 본문이 너무 길어지거나 복잡해지면, 차라리 별도의 명명된 함수나 함수 객체 클래스로 분리하는 것이 가독성 면에서 더 좋습니다. 람다는 짧고 한 번 사용할 함수에 가장 적합합니다.
- 생명 주기: 참조 캡처(
[&],[&var])를 사용할 때는 캡처된 변수의 생명 주기에 주의해야 합니다. 람다가 캡처된 변수보다 오래 살아남는 경우, 댕글링 참조(Dangling Reference) 문제가 발생하여 정의되지 않은 동작을 초래할 수 있습니다.#include <iostream> // 위험한 예시: auto create_lambda() { int local_var = 10; return [&local_var]() { // local_var를 참조 캡처 std::cout << local_var << std::endl; }; } int main() { auto lambda = create_lambda(); // create_lambda() 종료 시 local_var 소멸 // lambda(); // 여기서는 local_var가 이미 소멸된 상태이므로 위험! return 0; }
이러한 문제를 피하려면 값 캡처([=], [var])를 사용하거나, std::shared_ptr 같은 스마트 포인터를 사용하여 객체의 생명 주기를 관리해야 합니다.
lambda capture는 closure의 소유권과 lifetime을 결정한다
closure 생성의 값 복사/참조 연결과 local_var 소멸 후 접근 가능성 변화가 같은 원본 수명으로 이어진다.
C++ · CAPTURE LIFETIME
closure와 캡처 대상의 수명을 구분한다
반환·저장·비동기 전달로 closure가 오래 살 수 있으면 캡처 대상의 수명을 먼저 확인한다. 참조 캡처와 포인터 캡처는 소유권을 만들지 않는다.
값 복사 화살표는 생성 시점이다. 함수 종료 뒤 지역 원본이 소멸해도 정수 값 캡처는 남지만, 그 원본의 참조 캡처는 유효하지 않다. 포인터 값 캡처의 대상 수명은 별도이다.
값 캡처 · 독립 값
closure 생성 때 값이 복사된다. 원본 int가 바뀌어도 복사된 값은 그대로이다.
[x](int a) { return a + x; }mutable은 복사된 멤버의 변경을 허용한다. 포인터 값 복사는 대상의 수명을 연장하지 않는다.
참조 캡처 · 원본 수명 필요
원본을 읽거나 변경하며 참조 대상의 수명을 연장하지 않는다.
[&local_var]() {
return local_var;
}본문 create_lambda()에서 지역 변수 참조를 반환하면 함수 종료 후 해당 참조가 매달린다.
객체 캡처 · this / *this
[this]는 포인터 값을 저장하고, [*this]는 C++17부터 객체를 복사한다.
[this] // 원본 객체 필요
[*this] // 객체 복사복사본 안의 포인터나 참조가 다른 객체를 가리킨다면 그 대상의 수명도 별도로 확인한다.
소유권 캡처 · 명시적 자원
C++14 초기화 캡처로 unique_ptr를 이동하거나 shared_ptr를 복사할 수 있다.
[p = std::move(ptr)]() {
return *p;
}본문의 스마트 포인터 수명 관리 조언을 확장한 예시이다. 이동한 unique_ptr는 closure가 소유한다.
[]는 지역 값을 캡처하지 않는다. 전역·static 접근까지 금지하거나 순수 함수를 보장하는 표기는 아니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | 값 복사 화살표는 람다 생성 시점의 복사를 나타내며, 값 캡처 람다는 자체 내부 정수를 갖는다. 참조 캡처 람다는 원본 지역 정수에 접근한다. 함수 종료 뒤 원본이 소멸하면 참조 캡처 접근은 유효하지 않다. |
| 본문·주석 | 에서 지역 변수 참조를 반환하면 함수 종료 후 해당 참조는 유효하지 않다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 캡처 생성 local_var가 10인 상태에서 값 캡처와 참조 캡처를 비교한다. |
|
|
| 원본 수명 종료 함수 지역 local_var가 소멸한다. |
|
|
| 후속 사용 판단 남은 closure의 접근 가능성을 구분한다. |
|
|
lambda capture는 closure의 소유권과 lifetime을 결정한다
closure 생성의 값 복사/참조 연결과 local_var 소멸 후 접근 가능성 변화가 같은 원본 수명으로 이어진다.
C++ · CAPTURE LIFETIME
closure와 캡처 대상의 수명을 구분한다
반환·저장·비동기 전달로 closure가 오래 살 수 있으면 캡처 대상의 수명을 먼저 확인한다. 참조 캡처와 포인터 캡처는 소유권을 만들지 않는다.
값 복사 화살표는 생성 시점이다. 함수 종료 뒤 지역 원본이 소멸해도 정수 값 캡처는 남지만, 그 원본의 참조 캡처는 유효하지 않다. 포인터 값 캡처의 대상 수명은 별도이다.
값 캡처 · 독립 값
closure 생성 때 값이 복사된다. 원본 int가 바뀌어도 복사된 값은 그대로이다.
[x](int a) { return a + x; }mutable은 복사된 멤버의 변경을 허용한다. 포인터 값 복사는 대상의 수명을 연장하지 않는다.
참조 캡처 · 원본 수명 필요
원본을 읽거나 변경하며 참조 대상의 수명을 연장하지 않는다.
[&local_var]() {
return local_var;
}본문 create_lambda()에서 지역 변수 참조를 반환하면 함수 종료 후 해당 참조가 매달린다.
객체 캡처 · this / *this
[this]는 포인터 값을 저장하고, [*this]는 C++17부터 객체를 복사한다.
[this] // 원본 객체 필요
[*this] // 객체 복사복사본 안의 포인터나 참조가 다른 객체를 가리킨다면 그 대상의 수명도 별도로 확인한다.
소유권 캡처 · 명시적 자원
C++14 초기화 캡처로 unique_ptr를 이동하거나 shared_ptr를 복사할 수 있다.
[p = std::move(ptr)]() {
return *p;
}본문의 스마트 포인터 수명 관리 조언을 확장한 예시이다. 이동한 unique_ptr는 closure가 소유한다.
[]는 지역 값을 캡처하지 않는다. 전역·static 접근까지 금지하거나 순수 함수를 보장하는 표기는 아니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | 값 복사 화살표는 람다 생성 시점의 복사를 나타내며, 값 캡처 람다는 자체 내부 정수를 갖는다. 참조 캡처 람다는 원본 지역 정수에 접근한다. 함수 종료 뒤 원본이 소멸하면 참조 캡처 접근은 유효하지 않다. |
| 본문·주석 | 에서 지역 변수 참조를 반환하면 함수 종료 후 해당 참조는 유효하지 않다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 캡처 생성 local_var가 10인 상태에서 값 캡처와 참조 캡처를 비교한다. |
|
|
| 원본 수명 종료 함수 지역 local_var가 소멸한다. |
|
|
| 후속 사용 판단 남은 closure의 접근 가능성을 구분한다. |
|
|
람다를 저장하거나 비동기로 넘길 가능성이 있다면 [&] 같은 기본 참조 캡처를 먼저 의심해야 합니다.
캡처 목록을 좁히고, 값 캡처나 소유권이 드러나는 스마트 포인터 캡처로 수명을 분리하는 편이 안전합니다.
예시: 람다 복사본의 상태는 어디에 남을까?
mutable은 값으로 캡처한 람다 내부 상태를 바꿀 수 있게 합니다. 원래 바깥 변수를 참조 캡처로 바꾸는 기능은 아닙니다. 다음 C++17 파일은 호출 순서를 문장별로 고정해 두 람다 객체의 상태를 비교합니다.
#include <iostream>
int main() {
int x = 10;
auto counter = [x]() mutable { return ++x; };
auto other = counter;
int first = counter();
int second = counter();
int copiedFirst = other();
std::cout << first << ' ' << second << ' ' << copiedFirst << ' ' << x << '\n';
}출력은 11 12 11 10입니다. 람다를 만들 때 counter의 캡처 값은 10이고, other를 복사할 때도 10입니다. 첫 두 호출은 counter 안의 정수만 11, 12로 바꾸고, 세 번째 호출은 other 안의 정수를 처음으로 11로 바꿉니다. 바깥 x는 여전히 10입니다. 캡처 대상이 정수인 이 사례에서는 두 복사본의 상태가 독립적입니다. 포인터나 shared_ptr를 값 캡처하면 포인터 값·소유자를 복사하는 것이므로 가리키는 객체까지 별도로 복제된다는 뜻은 아닙니다.
앞의 위험한 반환 람다에서 지역 정수를 참조 캡처하면 함수 종료 뒤 참조 대상이 사라집니다. 정수 값을 반환 람다 안에 보관하려는 목적이라면 [local_var]로 값 캡처할 수 있습니다. 그 경우에도 캡처한 값이 포인터라면 대상 객체의 수명은 별도로 확인해야 합니다.
독립 연습
위 코드의 x를 3으로 바꾸고, auto other = counter;를 첫 번째 counter() 호출 직후로 옮깁니다. 이후 두 번째 counter()와 첫 번째 other()를 호출했을 때 세 반환 값과 바깥 x는 얼마일까요? 별도 실험에서 캡처만 [&x]로 바꾸고 같은 순서로 실행하면 무엇이 달라질까요? 두 실험 모두 x가 살아 있는 main 안에서만 호출합니다.
캡처와 복사 시점을 나누는 풀이
값 캡처에서는 첫 호출이 4를 반환하고 그때의 내부 값 4를 other가 복사합니다. 두 번째 counter()는 5, 첫 번째 other()도 5를 반환하며 바깥 x는 3입니다. 참조 캡처에서는 두 람다 모두 같은 바깥 x를 참조하므로 호출 결과는 4, 5, 6이고 마지막 x는 6입니다. 람다를 복사한 시점은 값 캡처의 내부 상태를 정하지만 참조 대상 정수 자체를 복제하지는 않습니다.
이 차이는 클로저의 호출 연산자·복사 생성과 값·참조 캡처 및 대상 수명의 규칙에 해당합니다.