icon
14장 : 현대 C++ 기능

람다 표현식

이번 장에서는 현대 C++의 또 다른 혁신적인 기능인 람다 표현식(Lambda Expressions) 에 대해 알아보겠습니다.

람다 표현식은 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) 상세 분석

캡처 절은 람다 표현식이 외부 스코프의 변수를 어떻게 사용할 것인지 결정합니다.

  1. [] (아무것도 캡처하지 않음)

    • 람다 내부에서 외부 스코프의 변수를 사용할 수 없습니다. 순수한 함수처럼 동작합니다.
    아무것도 캡처하지 않는 람다
    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;
    }
  2. [var] (값으로 캡처 - by value)

    • 외부 변수 var의 값을 람다 객체 생성 시 복사합니다. 람다 본문 내에서 var의 값을 변경해도 외부의 원본 var에는 영향을 주지 않습니다. 기본적으로 const로 캡처됩니다.
    값으로 캡처하는 람다
    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 키워드를 붙여야 합니다.
    mutable 키워드를 사용한 값 캡처
    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;
    }
  3. [&var] (참조로 캡처 - by reference)

    • 외부 변수 var를 참조로 캡처합니다. 람다 본문 내에서 var를 변경하면 외부의 원본 var도 변경됩니다.
    참조로 캡처하는 람다
    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;
    }
  4. [=] (모든 변수를 값으로 캡처)

    • 람다 본문 내에서 사용되는 모든 외부 변수를 값으로 캡처합니다. (예외: this 포인터는 항상 참조로 캡처됩니다.)
    모든 변수를 값으로 캡처하는 람다
    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;
    }
  5. [&] (모든 변수를 참조로 캡처)

    • 람다 본문 내에서 사용되는 모든 외부 변수를 참조로 캡처합니다.
    모든 변수를 참조로 캡처하는 람다
    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;
    }
  6. 혼합 캡처: 특정 변수는 값으로, 다른 변수는 참조로 캡처할 수 있습니다.

    혼합 캡처 예시
    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;
    }
  7. [this] (C++11) / *this (C++17): 멤버 함수 내에서 람다를 정의할 때, 람다 내부에서 클래스의 멤버 변수나 멤버 함수에 접근하려면 this 포인터를 캡처해야 합니다.

    • [this]: this 포인터를 참조로 캡처합니다.
    • [*this] (C++17): 현재 객체(*this)를 값으로 복사하여 캡처합니다. 복사본이므로 원본 객체와 람다의 생명 주기를 분리할 때 유용합니다.

람다 표현식의 활용 사례

람다 표현식은 주로 STL 알고리즘과 함께 사용될 때 그 진가를 발휘합니다.

std::sort와 람다 사용 예시
#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;
}
std::find_if와 람다 사용 예시
#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를 사용해야 람다 객체를 변수에 저장하거나 다른 함수로 전달할 수 있습니다.

auto와 람다의 조합 예시
#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) 문제가 발생하여 정의되지 않은 동작을 초래할 수 있습니다.
    // 위험한 예시:
    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 같은 스마트 포인터를 사용하여 객체의 생명 주기를 관리해야 합니다.