본문 바로가기

람다 표현식

캡처·매개변수·반환 타입으로 람다를 구성하고 지역 상태와 STL 알고리즘을 연결하는 패턴 및 주의점을 익힙니다.

함수 객체에서 알고리즘에 넘길 동작을 클래스로 만들었는데, 짧은 조건 하나를 위해 클래스를 정의하는 것은 번거롭습니다. C++11의 람다 표현식(lambda expression)은 필요한 곳에서 바로 이름 없는 함수 객체를 만들어 줍니다. 컴파일러가 operator()를 가진 클래스를 대신 만들고, 그 객체를 클로저(closure)라고 부릅니다.

이 글은 람다의 문법, 바깥 변수를 담는 캡처의 여러 형태, 알고리즘과 함께 쓰는 방법, 캡처한 대상의 수명, 그리고 C++14 이후의 초기화 캡처와 제네릭 람다를 다룹니다.

람다의 구성

람다는 캡처, 매개변수 목록, 반환 타입, 본문으로 이루어집니다. 람다에는 이름이 없으므로 일반 함수의 이름 자리에 캡처를 적는 대괄호가 오고, 컴파일러는 이 대괄호를 보고 람다가 시작한다는 것을 압니다. 반환 타입은 일반 함수처럼 앞에 두지 않고, 매개변수 목록 뒤에 화살표 ->와 함께 적습니다.

람다 형식
[캡처](매개변수) -> 반환타입 {
    // 본문
}

캡처는 람다 밖의 지역 변수를 본문에서 쓸 수 있게 담는 방법을 정하고, 매개변수와 본문은 일반 함수와 같습니다. 반환 타입은 대부분 생략하며, 그러면 return 식으로 정해집니다. 람다의 타입은 컴파일러만 아는 고유한 타입이어서, 변수에 담을 때는 auto add = [](int a, int b) { return a + b; };처럼 auto로 받습니다.

더 깊이: 캡처 없는 람다가 할 수 있는 일

캡처가 없는 람다의 클로저 타입에는 같은 서명의 함수 포인터로 바꾸어 주는 변환 함수가 있습니다. 그래서 int (*fp)(int, int) = [](int a, int b) { return a + b; };처럼 쓸 수 있고, 함수 포인터를 받는 C 라이브러리의 콜백 자리에도 캡처 없는 람다를 그대로 넘길 수 있습니다. 하나라도 캡처하면 람다는 그 값을 멤버로 가져야 하므로 이 변환은 생기지 않습니다.

C++17부터는 람다의 operator()가 constexpr 함수의 조건을 만족하면 따로 적지 않아도 constexpr이 되어, 상수 표현식 안에서도 람다를 부를 수 있습니다. C++20부터는 캡처 없는 람다의 클로저 타입이 기본 생성자와 복사 대입 연산자도 가집니다. 그래서 auto cmp = [](int a, int b) { return a > b; };를 만든 뒤 std::set<int, decltype(cmp)>처럼 타입만 넘겨도 컨테이너가 비교 객체를 스스로 만들 수 있는데, 그 전에는 기본 생성자가 없어 cmp 객체를 생성자 인자로 함께 넘겨야 했습니다. 자세한 규칙은 cppreference의 람다 표현식 문서에 있습니다.

캡처

캡처는 바깥의 변수를 값으로 복사해 담을지, 원래 변수를 참조로 쓸지를 정합니다.

캡처뜻
[]아무것도 캡처하지 않음
[x]x를 람다를 만들 때의 값으로 복사
[&x]x를 참조로 담아 원래 변수를 씀
[=]본문에서 쓰는 지역 변수를 모두 값으로
[&]본문에서 쓰는 지역 변수를 모두 참조로
[x, &y]x는 값으로, y는 참조로
[this]객체를 가리키는 this 포인터를 담음
[*this]C++17, 객체 자체를 값으로 복사
captures.cpp
#include <iostream>

int main() {
    int x = 10;
    auto byValue = [x](int a) { return a + x; };
    auto counter = [x]() mutable { return ++x; };
    auto byRef = [&x](int a) { ++x; return a + x; };

    x = 20;
    std::cout << byValue(5) << '\n';
    std::cout << counter() << ' ' << counter() << '\n';
    std::cout << byRef(5) << ' ' << x << '\n';
}
15
11 12
26 21

byValue는 만들 때의 x 값 10을 복사해 두었으므로, 뒤에 x를 20으로 바꿔도 5 + 10인 15를 돌려줍니다. 값으로 담은 변수는 기본으로 람다 안에서 바꿀 수 없는데, mutable을 붙이면 람다가 가진 복사본을 바꿀 수 있어 counter는 호출할 때마다 11, 12로 늘어납니다. 이 복사본은 원래의 x와 별개입니다. byRef는 원래의 x를 참조하므로 20을 21로 늘리고 26을 돌려주며, 그 뒤에 출력한 x도 21입니다.

[=]나 [&]처럼 한꺼번에 담는 캡처는 편하지만, 무엇이 어떤 방식으로 담겼는지 코드에서 드러나지 않습니다. 담는 변수가 적다면 [x, &y]처럼 하나씩 적는 편이 의도를 보여 줍니다. 전역 변수와 static 변수는 캡처하지 않아도 본문에서 쓸 수 있으므로, []가 바깥 상태를 전혀 쓰지 않는다는 뜻은 아닙니다.

알고리즘과 람다

람다는 알고리즘에 넘기는 조건이나 비교로 가장 많이 쓰입니다. 캡처를 쓰면 기준값처럼 실행 중에 정해지는 값을 조건에 넣을 수 있습니다.

find_if_lambda.cpp
#include <algorithm>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> numbers{10, 25, 30, 45, 50};
    int threshold = 40;

    auto it = std::find_if(numbers.begin(), numbers.end(),
                           [threshold](int n) { return n > threshold; });
    if (it != numbers.end()) {
        std::cout << *it << " at " << (it - numbers.begin()) << '\n';
    }

    std::vector<int> scores{85, 92, 78, 65, 95, 80};
    int minScore = 90;
    auto passed = std::count_if(scores.begin(), scores.end(),
                                [minScore](int s) { return s >= minScore; });
    std::cout << passed << '\n';
}
45 at 3
2

아래 그림처럼 std::find_if는 앞의 원소부터 람다를 호출해, 10, 25, 30에서는 false를 받고 45에서 처음 true를 받아 그 원소의 반복자를 돌려줍니다. 처음 참이 된 곳에서 멈추므로 50은 검사하지 않습니다. 알고리즘은 순회를 맡고, 람다는 담아 둔 threshold와 현재 원소로 판단만 합니다.

STL 알고리즘은 closure를 반복 호출해 결과를 만든다

find_if가 10·25·30에서 false, 45에서 true 뒤 종료하며 50은 검사하지 않는 구체 실행 추적이다.

C++ · LAMBDA ALGORITHM

find_if는 closure를 호출해 첫 일치 위치를 찾는다

알고리즘은 순회 구조를, closure는 capture한 상태와 현재 원소에 대한 판단을 담당한다.

find_if의 람다 호출과 첫 true 반환10,25,30을 순서대로 검사해 false를 얻는 반복 뒤45에서true를 얻고 그 원소 반복자를 호출자에게 반환한다.50은 검사하지 않는다. 호출자find_if람다LOOP: 10·25·30범위·람다원소 검사false45 검사true45 반복자
find_if의 람다 호출과 첫 true 반환10,25,30을 순서대로 검사해 false를 얻는 반복 뒤45에서true를 얻고 그 원소 반복자를 호출자에게 반환한다.50은 검사하지 않는다.호출자find_if람다LOOP: 10·25·30범위·람다원소 검사false45 검사true45 반복자

보조 설명 · 코드와 수명 조건

  1. 호출 지점

    범위와 lambda closure를 알고리즘에 넘긴다.

  2. 순회

    알고리즘이 현재 원소로 closure를 호출한다.

  3. 판단

    closure가 capture와 원소를 이용해 bool을 돌려준다.

  4. 결과

    첫 일치 위치를 반환하며 없으면 end를 반환한다.

본문 {10, 25, 30, 45, 50}에서 n > 30을 검사하면 첫 일치는 45이다. 순회와 판단은 알고리즘 실행 범위 안에서 이루어진다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
원 도표의 제목·노드·관계·조건·주석
범위원문
관계·조건 설명10,25,30을 순서대로 검사해 false를 얻는 반복 뒤 45에서 true를 얻고 그 원소 반복자를 호출자에게 반환한다. 50은 검사하지 않는다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
거짓 후보들

find_if가 앞의 요소부터 조건을 검사한다.

  • 입력: 10,25,30,45,50
  • 앞의 검사: 10·25·30에서 n>30=false
  • 조건이 거짓이면 다음 요소로 계속 이동한다.
첫 참

45에서 조건이 처음 성립한다.

  • 현재 요소: 45
  • n>30: true
  • find_if는 첫 성공 위치에서 탐색을 끝낸다.
반환과 미검사

45를 가리키는 반복자를 반환한다.

  • 반환 위치: 45
  • 50: 검사하지 않음
  • 첫 성공 이후 요소를 계속 검사할 필요가 없다.

STL 알고리즘은 closure를 반복 호출해 결과를 만든다

find_if가 10·25·30에서 false, 45에서 true 뒤 종료하며 50은 검사하지 않는 구체 실행 추적이다.

C++ · LAMBDA ALGORITHM

find_if는 closure를 호출해 첫 일치 위치를 찾는다

알고리즘은 순회 구조를, closure는 capture한 상태와 현재 원소에 대한 판단을 담당한다.

find_if의 람다 호출과 첫 true 반환10,25,30을 순서대로 검사해 false를 얻는 반복 뒤45에서true를 얻고 그 원소 반복자를 호출자에게 반환한다.50은 검사하지 않는다. 호출자find_if람다LOOP: 10·25·30범위·람다원소 검사false45 검사true45 반복자
find_if의 람다 호출과 첫 true 반환10,25,30을 순서대로 검사해 false를 얻는 반복 뒤45에서true를 얻고 그 원소 반복자를 호출자에게 반환한다.50은 검사하지 않는다.호출자find_if람다LOOP: 10·25·30범위·람다원소 검사false45 검사true45 반복자

보조 설명 · 코드와 수명 조건

  1. 호출 지점

    범위와 lambda closure를 알고리즘에 넘긴다.

  2. 순회

    알고리즘이 현재 원소로 closure를 호출한다.

  3. 판단

    closure가 capture와 원소를 이용해 bool을 돌려준다.

  4. 결과

    첫 일치 위치를 반환하며 없으면 end를 반환한다.

본문 {10, 25, 30, 45, 50}에서 n > 30을 검사하면 첫 일치는 45이다. 순회와 판단은 알고리즘 실행 범위 안에서 이루어진다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.

관계도의 보충 설명

원 도표의 제목·노드·관계·조건·주석
범위원문
관계·조건 설명10,25,30을 순서대로 검사해 false를 얻는 반복 뒤 45에서 true를 얻고 그 원소 반복자를 호출자에게 반환한다. 50은 검사하지 않는다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
거짓 후보들

find_if가 앞의 요소부터 조건을 검사한다.

  • 입력: 10,25,30,45,50
  • 앞의 검사: 10·25·30에서 n>30=false
  • 조건이 거짓이면 다음 요소로 계속 이동한다.
첫 참

45에서 조건이 처음 성립한다.

  • 현재 요소: 45
  • n>30: true
  • find_if는 첫 성공 위치에서 탐색을 끝낸다.
반환과 미검사

45를 가리키는 반복자를 반환한다.

  • 반환 위치: 45
  • 50: 검사하지 않음
  • 첫 성공 이후 요소를 계속 검사할 필요가 없다.

캡처와 수명

참조로 담은 람다는 원래 변수가 살아 있을 때만 쓸 수 있습니다. 람다를 함수에서 돌려주거나, 저장해 두었다가 나중에 부르거나, 다른 스레드에 넘기면 람다가 원래 변수보다 오래 남을 수 있습니다.

capture_lifetime.cpp
#include <functional>
#include <iostream>

std::function<int()> makeByValue() {
    int localVar = 10;
    return [localVar]() { return localVar; };
}

std::function<int()> makeByReference() {
    int localVar = 10;
    return [&localVar]() { return localVar; }; // 위험: 함수가 끝나면 localVar가 사라짐
}

int main() {
    auto safe = makeByValue();
    std::cout << safe() << '\n';

    auto dangling = makeByReference();
    // dangling(); // 정의되지 않은 동작
}
10

아래 그림처럼 값으로 담은 람다는 만들 때 10을 자기 안에 복사해 두므로, 함수가 끝나 localVar가 사라져도 안전하게 10을 돌려줍니다. 참조로 담은 람다는 사라진 localVar를 가리키므로, 호출하면 정의되지 않은 동작입니다. 이런 코드는 실행해서 확인하지 말고, 람다가 담은 대상이 람다보다 오래 사는지로 판단합니다.

lambda capture는 closure의 소유권과 lifetime을 결정한다

closure 생성의 값 복사/참조 연결과 local_var 소멸 후 접근 가능성 변화가 같은 원본 수명으로 이어진다.

C++ · CAPTURE LIFETIME

closure와 캡처 대상의 수명을 구분한다

반환·저장·비동기 전달로 closure가 오래 살 수 있으면 캡처 대상의 수명을 먼저 확인한다. 참조 캡처와 포인터 캡처는 소유권을 만들지 않는다.

캡처 객체 내부 값과 빌린 원본의 수명값 복사 화살표는 람다 생성 시점이고 자체 내부 정수를 갖는다. 참조 캡처 람다는 원본 지역 정수에 접근한다. 함수 종료 뒤 원본이 소멸하면 참조 캡처 접근은 유효하지 않다.값 캡처 람다생성 시 값 복사참조내부 정수자체 값 10원본 지역 정수local_var = 10참조 캡처 람다원본 수명 필요
캡처 객체 내부 값과 빌린 원본의 수명값 복사 화살표는 람다 생성 시점이고 자체 내부 정수를 갖는다. 참조 캡처 람다는 원본 지역 정수에 접근한다. 함수 종료 뒤 원본이 소멸하면 참조 캡처 접근은 유효하지 않다.값 캡처 람다생성 시값 복사참조내부 정수자체 값 10원본 지역 정수local_var = 10참조 캡처 람다원본 수명 필요

값 복사 화살표는 생성 시점이다. 함수 종료 뒤 지역 원본이 소멸해도 정수 값 캡처는 남지만, 그 원본의 참조 캡처는 유효하지 않다. 포인터 값 캡처의 대상 수명은 별도이다.

값 캡처 · 독립 값

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인 상태에서 값 캡처와 참조 캡처를 비교한다.

  • 값 캡처: 10의 사본
  • 참조 캡처: local_var 연결
  • 값과 참조가 closure에 보관하는 관계는 다르다.
원본 수명 종료

함수 지역 local_var가 소멸한다.

  • local_var: 수명 종료
  • 값 캡처: 사본 유지
  • 지역 범위를 나가도 closure의 사본은 closure 수명 동안 유지된다.
후속 사용 판단

남은 closure의 접근 가능성을 구분한다.

  • 값 캡처: 사용 가능
  • 참조 캡처: 유효하지 않은 참조
  • 소멸한 원본을 참조로 읽는 실행은 허용 사례로 수행하지 않는다.

lambda capture는 closure의 소유권과 lifetime을 결정한다

closure 생성의 값 복사/참조 연결과 local_var 소멸 후 접근 가능성 변화가 같은 원본 수명으로 이어진다.

C++ · CAPTURE LIFETIME

closure와 캡처 대상의 수명을 구분한다

반환·저장·비동기 전달로 closure가 오래 살 수 있으면 캡처 대상의 수명을 먼저 확인한다. 참조 캡처와 포인터 캡처는 소유권을 만들지 않는다.

캡처 객체 내부 값과 빌린 원본의 수명값 복사 화살표는 람다 생성 시점이고 자체 내부 정수를 갖는다. 참조 캡처 람다는 원본 지역 정수에 접근한다. 함수 종료 뒤 원본이 소멸하면 참조 캡처 접근은 유효하지 않다.값 캡처 람다생성 시 값 복사참조내부 정수자체 값 10원본 지역 정수local_var = 10참조 캡처 람다원본 수명 필요
캡처 객체 내부 값과 빌린 원본의 수명값 복사 화살표는 람다 생성 시점이고 자체 내부 정수를 갖는다. 참조 캡처 람다는 원본 지역 정수에 접근한다. 함수 종료 뒤 원본이 소멸하면 참조 캡처 접근은 유효하지 않다.값 캡처 람다생성 시값 복사참조내부 정수자체 값 10원본 지역 정수local_var = 10참조 캡처 람다원본 수명 필요

값 복사 화살표는 생성 시점이다. 함수 종료 뒤 지역 원본이 소멸해도 정수 값 캡처는 남지만, 그 원본의 참조 캡처는 유효하지 않다. 포인터 값 캡처의 대상 수명은 별도이다.

값 캡처 · 독립 값

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인 상태에서 값 캡처와 참조 캡처를 비교한다.

  • 값 캡처: 10의 사본
  • 참조 캡처: local_var 연결
  • 값과 참조가 closure에 보관하는 관계는 다르다.
원본 수명 종료

함수 지역 local_var가 소멸한다.

  • local_var: 수명 종료
  • 값 캡처: 사본 유지
  • 지역 범위를 나가도 closure의 사본은 closure 수명 동안 유지된다.
후속 사용 판단

남은 closure의 접근 가능성을 구분한다.

  • 값 캡처: 사용 가능
  • 참조 캡처: 유효하지 않은 참조
  • 소멸한 원본을 참조로 읽는 실행은 허용 사례로 수행하지 않는다.

[this]도 같은 주의가 필요합니다. this 포인터만 담으므로 객체를 복사하지도, 객체의 수명을 늘리지도 않으며, 객체가 사라진 뒤 람다를 부르면 허상 포인터를 쓰게 됩니다. 람다가 객체보다 오래 남을 수 있다면 C++17의 [*this]로 객체를 복사해 담습니다. [=]가 this를 몰래 담는 것은 C++20부터 권장되지 않으므로, 멤버를 쓸 때는 [this]나 [*this]를 직접 적습니다.

초기화 캡처와 제네릭 람다

C++14부터는 캡처 안에서 새 이름을 만들어 원하는 값으로 초기화할 수 있으며, 이를 초기화 캡처(init capture)라고 합니다. 복사할 수 없는 std::unique_ptr도 이동해 담을 수 있습니다. 매개변수를 auto로 적으면 여러 타입을 받는 제네릭 람다(generic lambda)가 됩니다.

init_capture_generic.cpp
#include <iostream>
#include <memory>
#include <string>
#include <utility>

int main() {
    auto owner = std::make_unique<int>(7);
    auto reader = [p = std::move(owner)]() { return *p; };
    std::cout << reader() << ' ' << std::boolalpha << (owner == nullptr) << '\n';

    auto add = [](auto a, auto b) { return a + b; };
    std::cout << add(1, 2) << ' ' << add(std::string("a"), "b") << '\n';
}
7 true
3 ab

[p = std::move(owner)]는 람다 안에 p라는 멤버를 만들고 owner가 가진 메모리의 소유를 옮겨 담으므로, owner는 nullptr이 되고 메모리는 람다가 사라질 때 해제됩니다. add는 함수 템플릿처럼 호출할 때마다 인자의 타입에 맞는 operator()가 만들어져, 정수끼리도 문자열끼리도 더할 수 있습니다. C++20부터는 []<typename T>(T a, T b) { ... }처럼 템플릿 매개변수를 직접 적을 수도 있습니다.

정리

  • 람다는 그 자리에서 이름 없는 함수 객체인 클로저를 만들며, 타입이 고유하므로 auto로 받습니다.
  • 값 캡처는 만들 때의 값을 복사하고, 참조 캡처는 원래 변수를 쓰며, mutable은 람다가 가진 복사본을 바꿀 수 있게 합니다.
  • 알고리즘은 순회를 맡고 람다는 판단을 맡으며, 캡처로 실행 중에 정해지는 값을 조건에 넣을 수 있습니다.
  • 참조 캡처와 [this]는 대상의 수명을 늘리지 않으므로, 람다가 대상보다 오래 남을 수 있다면 값으로 담습니다.
  • C++14의 초기화 캡처는 새 멤버를 원하는 값으로 만들고, auto 매개변수는 여러 타입을 받는 제네릭 람다를 만듭니다.

연습 문제

1. 캡처의 시점

captures.cpp에서 x = 20;을 람다들을 만들기 전으로 옮기면, 세 출력 줄은 어떻게 바뀌나요?

해설 보기

byValue와 counter가 만들 때 20을 복사하므로 첫 줄은 25, 둘째 줄은 21 22가 됩니다. byRef는 원래의 x를 쓰는데 x는 이미 20이므로, 셋째 줄은 x가 21이 된 뒤의 26 21로 그대로입니다. 값 캡처는 만드는 순간의 값을, 참조 캡처는 부르는 순간의 값을 씁니다.

2. 기준값 바꾸기

find_if_lambda.cpp에서 threshold를 25로 바꾸면 첫 줄은 무엇인가요? 60으로 바꾸면 어떻게 되나요?

해설 보기

25보다 큰 첫 원소는 30이므로 30 at 2가 출력됩니다. 60으로 바꾸면 조건을 만족하는 원소가 없어 find_if가 end()를 돌려주고, if가 거짓이 되어 첫 줄은 출력되지 않습니다. 둘째 줄의 2는 그대로입니다.

3. 계수기를 돌려주는 함수

다음 함수가 돌려준 람다를 두 번 부르면 어떻게 되나요? 어떻게 고치면 호출할 때마다 1, 2를 돌려주나요?

make_counter.cpp
#include <functional>

std::function<int()> makeCounter() {
    int count = 0;
    return [&count]() { return ++count; };
}
해설 보기

count는 makeCounter가 끝나면 사라지므로, 돌려받은 람다를 부르면 사라진 변수를 참조해 정의되지 않은 동작이 됩니다. return [count]() mutable { return ++count; };처럼 값으로 담고 mutable을 붙이면, 람다가 자기 복사본을 가지고 있어 부를 때마다 1, 2를 돌려줍니다.

다음 글 스마트 포인터에서는 동적 메모리의 소유를 맡는 std::unique_ptr, std::shared_ptr, std::weak_ptr를 비교하고, 공유 소유에서 생기는 순환 참조를 다룹니다.