Menu
Coddy logo textTech

람다 표현식 심화

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

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

전체 람다 구문은 다음과 같습니다: [capture](parameters) mutable -> return_type { body }. 캡처 절(capture clause)은 람다가 어떤 외부 변수에 접근할 수 있는지와 그 방법을 결정합니다:

#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
}

기본적으로, 값에 의해 캡처된(captured-by-value) 변수들은 람다 내부에서 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에서 초기화 캡처(init captures)가 추가되어, 람다 내에서 새로운 변수를 생성하거나 객체를 람다로 이동할 수 있게 되었습니다:

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

람다는 별도의 함수 객체를 정의하지 않고도 콜백, 사용자 정의 비교자 또는 이벤트 핸들러를 위해 동작을 매개변수로 전달해야 할 때 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. 기준 숫자 (정수, base)
    2. 배수 (정수, multiplier)

    EventDispatcher를 생성하고 세 개의 콜백을 추가하여 서로 다른 람다 캡처 기술을 시연하세요:

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

    모든 콜백을 추가한 후, fireAll()을 두 번 호출하여 여러 번의 호출에 걸쳐 서로 다른 캡처 모드가 어떻게 동작하는지 확인하세요. 두 번의 fireAll() 호출 사이에 구분선으로 ---를 출력하세요.

    마지막으로, 두 라운드가 모두 끝난 후 main의 multiplier 변수 최종 값을 출력하여 참조 캡처가 어떻게 영향을 미쳤는지 보여주세요: Final multiplier: [multiplier]

예를 들어, 입력이 105인 경우:

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

입력이 420인 경우:

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

주요 동작을 주목하세요: 값에 의한 캡처는 원래의 base를 변경하지 않고 유지하며, 참조에 의한 캡처는 main에 있는 실제 multiplier 변수를 수정합니다(호출될 때마다 누적됨). 또한 mutable 람다의 카운터는 동일한 람다 객체가 여러 번 호출될 때 내부 상태를 유지하며 증가하는 것을 관찰할 수 있습니다.

치트 시트

람다 표현식은 인라인으로 정의할 수 있는 익명 함수입니다. 전체 구문은 다음과 같습니다:

[capture](parameters) mutable -> return_type { body }

캡처 절 (Capture Clauses)

캡처 절은 람다가 외부의 어떤 변수에 접근할 수 있는지, 그리고 어떻게 접근하는지를 결정합니다:

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; };            // 두 방식 혼합

변경 가능한 람다 (Mutable Lambdas)

기본적으로 값에 의해 캡처된 변수들은 람다 내부에서 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 - 원본은 변경되지 않음

초기화 캡처 (Init Captures, C++14)

새로운 변수를 생성하거나 객체를 람다 내부로 이동시킵니다:

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

std::function과 함께 람다 사용하기

콜백이나 이벤트 핸들러를 위해 람다를 std::function에 저장합니다:

#include <functional>
#include <vector>

std::vector<std::function<void()>> callbacks;
callbacks.push_back([x]() { /* x 사용 */ });
callbacks.push_back([&y]() { /* y 수정 */ });

직접 해보기

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

using namespace std;

int main() {
    int base;
    int multiplier;
    cin >> base;
    cin >> multiplier;
    
    EventDispatcher dispatcher;
    
    // TODO: base를 값으로 캡처(BY VALUE)하는 람다를 추가하세요
    // 출력 형식: "Base value: [base]"
    
    // TODO: multiplier를 참조로 캡처(BY REFERENCE)하는 람다를 추가하세요
    // multiplier를 1 증가시킨 후, 다음을 출력해야 합니다: "Multiplier after increment: [multiplier]"
    
    // TODO: 0으로 초기화된 counter를 값으로 캡처하는 MUTABLE 람다를 추가하세요
    // counter를 증가시키고 다음을 출력해야 합니다: "Call count: [counter]"
    
    // TODO: 모든 콜백을 호출하기 위해 fireAll()을 호출하세요
    
    // TODO: 구분선으로 "---"를 출력하세요
    
    // TODO: fireAll()을 다시 호출하세요
    
    // TODO: 최종 multiplier 값을 출력하세요: "Final multiplier: [multiplier]"
    
    return 0;
}
quiz icon실력 점검

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

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