Menu
CoddyTech

람다 표현식 심화

Coddy C++ 여정의 객체 지향 프로그래밍 섹션에 포함된 레슨. 104개 중 85번째.

C++11에서 도입된 람다 표현식은 인라인으로 정의할 수 있는 익명 함수입니다. STL 알고리즘에서 기본적인 람다를 살펴봤지만, 전체 구문을 이해하면 변수를 캡처하고 변수에 접근하는 방식을 제어하는 강력한 기능을 활용할 수 있습니다.

람다의 전체 구문은 다음과 같습니다: [capture](parameters) mutable -> return_type { body }. 캡처 절은 람다가 바깥 변수를 어떤 방식으로 접근할 수 있는지 결정합니다:

#include <iostream>

int main() {
    int x = 10;
    int y = 20;
    
    auto byValue = [x]() { return x * 2; };        // x의 복사본
    auto byRef = [&y]() { y += 5; };               // y에 대한 참조
    auto allByValue = [=]() { return x + y; };     // 모두 복사
    auto allByRef = [&]() { x++; y++; };           // 모두 참조
    auto mixed = [x, &y]() { y += x; };            // 둘 다 혼합
    
    byRef();
    std::cout << y << "\n";  // 25
}

기본적으로 값으로 캡처된 변수는 람다 내부에서 const입니다. mutable 키워드를 사용하면 이러한 복사본을 수정할 수 있습니다.

int counter = 0;
auto increment = [counter]() mutable {
    return ++counter;  // 람다의 복사본을 수정함
};

std::cout << increment() << "\n";  // 1
std::cout << increment() << "\n";  // 2
std::cout << counter << "\n";      // 0 - 원본은 변경되지 않음

C++14에서는 초기화 캡처가 추가되어, 람다 안에 새 변수를 만들거나 객체를 이동할 수 있습니다:

auto ptr = std::make_unique<int>(42);
auto lambda = [p = std::move(ptr)]() {
    return *p;
};  // 소유권이 lambda로 이전됨

람다는 별도의 함수 객체를 정의하지 않고 콜백, 사용자 지정 비교 함수 또는 이벤트 핸들러를 위해 동작을 매개변수로 전달해야 할 때 OOP에서 특히 유용합니다.

challenge icon

챌린지

쉬움

서로 다른 캡처 방식으로 람다 표현식의 강력한 기능을 보여 주는 이벤트 처리 시스템을 만들어 봅시다. 콜백을 저장하고 호출하는 간단한 이벤트 디스패처를 만들어, 람다가 다양한 방식으로 외부 상태를 캡처하는 방법을 알아봅니다.

코드를 세 개의 파일로 구성합니다.

  • EventDispatcher.h: 이벤트 콜백을 관리하는 EventDispatcher 클래스를 정의합니다.

    디스패처는 std::function<void()>의 std::vector를 사용해 콜백을 저장해야 합니다. 다음 메서드를 포함하세요.

    • addCallback(std::function<void()> callback): 목록에 콜백을 추가합니다.
    • fireAll(): 저장된 모든 콜백을 순서대로 호출합니다.
    • clear(): 모든 콜백을 제거합니다.

    <functional>과 <vector>를 포함해야 합니다.

  • EventDispatcher.cpp: 디스패처의 메서드를 구현합니다. fireAll() 메서드는 모든 콜백을 순회하며 하나씩 호출하면 됩니다.
  • main.cpp: 입력 두 개를 읽습니다.
    1. 기준 숫자(정수)
    2. 배수(정수)

    EventDispatcher를 만들고 콜백 세 개를 추가해 서로 다른 람다 캡처 기법을 보여 줍니다.

    1. 기준 숫자를 값으로 캡처하고 다음을 출력하는 람다: Base value: [base]
    2. 배수를 참조로 캡처하고, 1 증가시킨 다음 다음을 출력하는 람다: Multiplier after increment: [multiplier]
    3. 카운터 변수(초깃값 0)를 값으로 캡처하고, 호출될 때마다 값을 증가시킨 다음 다음을 출력하는 mutable 람다: Call count: [counter]

    콜백을 모두 추가한 뒤 fireAll()을 두 번 호출하여 여러 번 호출할 때 캡처 방식이 어떻게 동작하는지 확인합니다. 두 fireAll() 호출 사이에 구분자로 ---를 출력합니다.

    마지막으로, 두 차례 호출을 마친 후 main의 배수 변수 최종 값을 출력하여 참조 캡처가 해당 변수에 미친 영향을 보여 줍니다: Final multiplier: [multiplier]

예를 들어, 입력이 10과 5라면 다음과 같습니다.

Base value: 10
Multiplier after increment: 6
Call count: 1
---
Base value: 10
Multiplier after increment: 7
Call count: 1
Final multiplier: 7

입력이 42와 0이라면 다음과 같습니다.

Base value: 42
Multiplier after increment: 1
Call count: 1
---
Base value: 42
Multiplier after increment: 2
Call count: 1
Final multiplier: 2

핵심 동작을 살펴보세요. 값으로 캡처하면 원래 기준 숫자가 그대로 유지되고, 참조로 캡처하면 main의 실제 배수 변수가 수정되어 호출할 때마다 값이 누적됩니다. 그리고 mutable 람다의 카운터는 매번 1로 출력됩니다. 이는 fireAll()이 콜백을 값으로 순회하기 때문입니다. 호출할 때마다 저장된 std::function의 새로운 복사본이 실행되며, 복사본에 캡처된 카운터는 0에서 시작해 1로 증가합니다. 따라서 저장된 콜백 자체의 상태는 변하지 않습니다.

직접 해보기

#include <iostream>
#include "EventDispatcher.h"

using namespace std;

int main() {
    int base;
    int multiplier;
    cin >> base;
    cin >> multiplier;
    
    EventDispatcher dispatcher;
    
    // TODO: base를 값으로 캡처하는 람다를 추가하세요
    // 다음을 출력해야 합니다: "Base value: [base]"
    
    // TODO: multiplier를 참조로 캡처하는 람다를 추가하세요
    // multiplier를 1 증가시킨 다음, "Multiplier after increment: [multiplier]"를 출력해야 합니다
    
    // TODO: 0으로 초기화된 카운터를 값으로 캡처하는 변경 가능한 람다를 추가하세요
    // 카운터를 증가시키고 "Call count: [counter]"를 출력해야 합니다
    
    // TODO: 모든 콜백을 실행하려면 fireAll()을 호출하세요
    
    // TODO: 구분자로 "---"를 출력하세요
    
    // TODO: fireAll()을 다시 호출하세요
    
    // TODO: multiplier의 최종 값을 출력하세요: "Final multiplier: [multiplier]"
    
    return 0;
}
quiz icon실력 점검

이 레슨에는 짧은 퀴즈가 포함되어 있습니다. 레슨을 시작해 문제를 풀고 진행 상황을 기록하세요.

객체 지향 프로그래밍의 모든 레슨

직접 연습해 보세요: 온라인 C++ 컴파일러