[13장] 람다 표현식과 함수형 프로그래밍의 접목

“[13장] 람다 표현식과 함수형 프로그래밍의 접목”은 람다 표현식의 기초부터 고급 활용법까지 다루며, 함수형 프로그래밍과의 연결점을 탐구합니다. 이 장을 통해 람다를 효과적으로 사용하는 방법과 함수형 프로그래밍의 장점을 깊이 있게 이해할 수 있습니다.

람다는 간결하게 만들고 싶은 욕망과 그 내부에서는 무한한 처리를 꿈꿀 수 있는 기술입니다.

람다 워밍업 – 간단 설명

윤성우의 열혈 C++ 프로그래밍, 오렌지미디어

C++에서 람다 표현식은 코드를 더욱 간결하고 표현력 있게 만드는 핵심 기능입니다. 이 글은 람다 표현식의 개념과 사용 방법, 그리고 함수형 프로그래밍과의 연계를 다룹니다.

람다 표현식의 소개

람다 표현식은 이름 없는 함수를 간단한 방법으로 정의할 수 있게 해주며, [capture](parameters) -> return_type { body } 형식을 사용합니다. 여기서 capture는 외부 변수를 람다 내부로 가져오는 메커니즘입니다.

함수형 프로그래밍과 람다

함수형 프로그래밍은 “무엇(What)”을 할 것인지에 초점을 맞추며, 람다 표현식은 이러한 접근 방식에 잘 부합합니다. 람다를 통해 알고리즘의 의도를 명확하게 드러낼 수 있습니다.

람다 표현식의 활용

람다 표현식은 표준 템플릿 라이브러리(STL)의 알고리즘과 함께 사용될 때 강력합니다. 예를 들어, std::sort, std::for_each와 같은 함수들은 람다 표현식과 결합하여 더욱 유연하고 표현력 있는 코드 작성을 가능하게 합니다.

C++에서 람다 표현식과 함수형 프로그래밍의 결합은 프로그래머에게 더 강력한 도구를 제공합니다. 이를 통해 코드의 가독성, 유지 보수성이 향상되며, 더 깔끔하고 효율적인 코드 작성이 가능해집니다.

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    // 람다 표현식을 사용하여 짝수만 필터링하고 출력
    std::cout << "짝수: ";
    std::for_each(numbers.begin(), numbers.end(), [](int n) {
        if (n % 2 == 0) {
            std::cout << n << " ";
        }
    });
    std::cout << std::endl;

    // 람다를 사용하여 모든 숫자에 2를 곱하고 출력
    std::cout << "모든 숫자에 2를 곱한 결과: ";
    std::transform(numbers.begin(), numbers.end(), numbers.begin(), [](int n) { return n * 2; });
    
    for (int n : numbers) {
        std::cout << n << " ";
    }
    std::cout << std::endl;

    return 0;
}

이 워밍업 예제에서는 C++ STL의 std::for_eachstd::transform 함수를 람다 표현식과 함께 사용하여, 각 요소에 대한 작업을 간결하고 효과적으로 수행합니다. 이는 C++에서 람다 표현식의 효율적인 사용 예를 보여줍니다.

C++20: 풍부한 예제로 익히는 핵심 기능, 인사이트

람다 구성 방식

람다 표현식은 C++에서 매우 유용한 기능으로, 익명 함수를 간결하게 작성할 수 있게 해줍니다. 람다 표현식을 구성하는 방법은 다음과 같습니다:

람다 표현식 구성 요소

  1. 캡처(Capture) 클로저: 람다 표현식이 정의된 범위의 변수들을 람다 함수 내에서 사용할 수 있도록 해줍니다. 캡처 방식에는 여러 가지가 있습니다:
    • [=]: 모든 외부 변수를 값으로 캡처합니다.
    • [&]: 모든 외부 변수를 참조로 캡처합니다.
    • [a, &b]: 변수 a는 값으로, 변수 b는 참조로 캡처합니다.
    • []: 외부 변수를 캡처하지 않습니다.
  2. 매개변수 목록(Parameter list): 일반 함수와 유사하게, 람다는 매개변수를 가질 수 있습니다. 매개변수는 괄호 () 안에 선언됩니다.
  3. 리턴 타입(Return type): 람다의 반환 타입은 -> 다음에 지정됩니다. 대부분의 경우 컴파일러가 반환 타입을 추론할 수 있기 때문에 생략이 가능합니다.
  4. 함수 본문(Function body): 중괄호 {} 안에 람다 함수의 본문을 작성합니다. 이 부분에는 람다 함수가 수행할 코드를 넣습니다.
#include <iostream>
#include <vector>

int main() {
    int x = 10;
    int y = 20;

    // 캡처 클로저 예제: x는 값으로, y는 참조로 캡처
    auto example = [x, &y]() {
        std::cout << "x: " << x << ", y: " << y << std::endl;
    };

    // 함수 호출
    example();

    // 외부 변수 변경
    y = 25;

    // 변경된 y 값으로 다시 함수 호출
    example();

    // 매개변수와 반환 타입을 가지는 람다
    auto multiply = [](int a, int b) -> int {
        return a * b;
    };

    // 함수 호출
    std::cout << "3 * 4 = " << multiply(3, 4) << std::endl;

    return 0;
}

이 코드에서는 다음과 같은 람다 표현식을 사용합니다:

초보자를 위한 C++ 200제:C++시작을위한최고의입문서! 설치부터문법배우고JSON응용까지레벨업!, 정보문화사
  • 첫 번째 람다에서는 x를 값으로, y를 참조로 캡처하여 사용합니다. y의 값이 변경되면 람다 내부에서도 이 변경 사항이 반영됩니다.
  • 두 번째 람다는 두 개의 매개변수를 받고, 곱셈 결과를 반환합니다. 이 경우 리턴 타입을 명시적으로 지정합니다(-> int).

이 예제들을 통해 람다 표현식의 다양한 사용 방법을 확인할 수 있습니다.

auto 키워드

C++에서 auto 키워드는 자동 타입 추론을 위해 사용됩니다. auto를 사용하면 컴파일러가 표현식의 타입을 추론하여 해당 변수의 타입을 결정합니다. 이는 코드 작성을 더 간결하게 하고, 특히 복잡한 타입 이름을 명시할 필요가 없게 만들어 코드의 가독성을 향상시킵니다.

auto 키워드의 사용 예시를 살펴보면 다음과 같습니다:

  • 변수 선언에서의 auto: 변수를 초기화할 때 auto를 사용하면, 초기화에 사용된 표현식의 타입에 따라 변수의 타입이 결정됩니다.
auto x = 5; // x는 int 타입으로 추론됩니다.
auto y = 3.14; // y는 double 타입으로 추론됩니다.
  • 람다 표현식에서의 auto: 람다 표현식을 변수에 할당할 때, auto를 사용하여 람다 표현식의 타입을 명시하지 않아도 됩니다. 람다 표현식의 타입은 컴파일러에 의해 자동으로 결정됩니다.
auto lambda = [](int a, int b) -> int { return a + b; };
// lambda의 타입은 컴파일러에 의해 결정됩니다.

auto의 이러한 사용은 특히 C++11 이후부터 매우 중요한 기능으로 자리 잡았으며, 모던 C++ 프로그래밍에서 널리 사용됩니다. auto는 코드를 단순화하고 유지 관리를 용이하게 하며, 타입 추론을 통해 컴파일러가 더 효율적으로 코드를 최적화할 수 있도록 돕습니다.

람다와 일반 함수의 성능 차이는?

전문가를 위한 C++ : C++20 병렬 알고리즘 파일시스템 제네릭 람다 디자인 패턴 객체지향의 원리를 익히는 확실한 방법 개정판, 한빛미디어

C++에서 람다 표현식과 일반 함수는 사용 목적과 문맥에 따라 다르게 활용되지만, 성능 측면에서는 대체로 유사합니다. 람다 표현식은 종종 인라인(inline)으로 처리되므로, 작은 함수에서는 일반 함수보다 더 나은 성능을 보일 수 있습니다. 그러나, 최신 컴파일러는 일반 함수에 대해서도 충분히 최적화를 수행하기 때문에, 두 방식 사이의 성능 차이는 미미하거나 없을 수 있습니다.

성능 비교

  1. 인라인 최적화: 람다 표현식은 컴파일 시점에 인라인으로 처리될 가능성이 높습니다. 이는 함수 호출 오버헤드를 줄여 성능을 향상시킬 수 있습니다. 반면, 일반 함수는 인라인 처리가 되지 않는 경우가 있을 수 있으며, 이는 추가적인 호출 오버헤드를 발생시킬 수 있습니다.
  2. 최적화 능력: 최신 컴파일러는 람다와 일반 함수 모두에 대해 고급 최적화 기법을 적용합니다. 따라서, 성능 차이는 컴파일러의 최적화 능력과 해당 코드의 특성에 크게 의존합니다.
  3. 사용 상황: 람다 표현식은 주로 STL 알고리즘, 이벤트 핸들러, 콜백 함수 등에서 사용됩니다. 일반 함수는 더 일반적인 상황에서 사용됩니다. 람다의 주요 이점은 코드의 간결성과 가독성에 있으며, 성능보다는 이러한 측면에서 그 가치가 큽니다.

성능 테스트 코드 예시

아래 코드는 람다 표현식과 일반 함수의 성능을 비교하는 간단한 예제입니다. std::chrono 라이브러리를 사용하여 실행 시간을 측정합니다.

#include <iostream>
#include <chrono>
#include <vector>
#include <numeric>

int normalFunction(int a, int b) {
    return a + b;
}

int main() {
    std::vector<int> numbers(1000000, 1); // 백만 개의 1로 초기화된 벡터

    // 람다 표현식
    auto lambda = [](int a, int b) { return a + b; };

    // 일반 함수 성능 측정
    auto start = std::chrono::high_resolution_clock::now();
    std::accumulate(numbers.begin(), numbers.end(), 0, normalFunction);
    auto end = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double> elapsed = end - start;
    std::cout << "Normal function time: " << elapsed.count() << " seconds\n";

    // 람다 표현식 성능 측정
    start = std::chrono::high_resolution_clock::now();
    std::accumulate(numbers.begin(), numbers.end(), 0, lambda);
    end = std::chrono::high_resolution_clock::now();
    elapsed = end - start;
    std::cout << "Lambda expression time: " << elapsed.count() << " seconds\n";

    return 0;
}
이것이 C++이다:강의 현장을 그대로 옮긴 C++ 입문서, 한빛미디어

이 코드는 std::accumulate 함수를 사용하여 벡터의 모든 요소를 더하는 작업을 람다 표현식과 일반 함수로 각각 수행한 후, 각각의 실행 시간을 측정합니다. 이를 통해 두 방식의 성능 차이를 비교할 수 있습니다.

성능 결과는 실행 환경, 컴파일러 최적화 수준, 코드의 복잡성 등에 따라 달라질 수 있으므로, 이 예제는 참조용으로만 사용하는 것이 좋습니다. 실제 애플리케이션에서의 성능 차이를 정확히 알기 위해서는 더 광범위한 테스트가 필요합니다.

람다를 사용하기 좋은 예들

STL 알고리즘과 함께 사용하기

컨테이너의 요소 필터링, 변형, 순회 등에 std::sort, std::for_each, std::transform 등과 같은 STL 알고리즘과 함께 람다를 사용할 수 있습니다.

std::vector<int> v = {1, 2, 3, 4, 5};

std::sort(v.begin(), v.end(), [](int a, int b) { return a > b; });

std::for_each(v.begin(), v.end(), [](int n) { std::cout << n << ' '; });
std::cout << std::endl;

std::transform(v.begin(), v.end(), v.begin(), [](int n) { return n * 2; });
  • std::sort: 벡터 v를 내림차순으로 정렬합니다. 람다는 두 요소를 비교하여 정렬 기준을 제공합니다.
  • std::for_each: 벡터의 각 요소를 순회하며 출력합니다. 람다는 각 요소를 받아 출력하는 역할을 합니다.
  • std::transform: 벡터의 각 요소에 2를 곱하여 같은 위치에 저장합니다. 람다는 각 요소에 대한 연산을 정의합니다.

콜백 함수로 사용하기

GUI 프로그래밍 또는 이벤트 기반 시스템에서 이벤트 핸들러나 콜백 함수로 람다를 사용합니다.

void eventHandler(std::function<void()> callback) {
    std::cout << "이벤트 발생" << std::endl;
    callback();
}

eventHandler([]() { std::cout << "콜백 실행" << std::endl; });

eventHandler 함수는 콜백 함수를 매개변수로 받습니다. 람다 표현식은 이벤트 발생 시 실행할 코드를 정의합니다.

스레드 생성 시 사용하기

멀티스레딩 환경에서 std::thread를 생성할 때 람다 표현식을 사용하여 스레드가 수행할 작업을 정의할 수 있습니다.

std::thread t([]() {
    std::cout << "스레드에서 실행" << std::endl;
});

t.join();
  • std::thread: 새로운 스레드를 생성하며, 람다는 스레드에서 실행될 작업을 정의합니다.
  • t.join(): 메인 스레드가 새로 생성된 스레드의 작업이 끝날 때까지 기다리도록 합니다.

지연 실행(lazy evaluation)

함수나 연산의 실행을 지연시키기 위해 람다를 사용할 수 있으며, 이는 특히 리소스가 많이 소모되는 연산에 유용합니다.

#include <iostream>
#include <functional>

std::function<int()> lazyAdd(int a, int b) {
    return [=]() { return a + b; };
}

int main() {
    auto lazyResult = lazyAdd(4, 5);
    std::cout << "계산되지 않음" << std::endl;
    std::cout << "결과: " << lazyResult() << std::endl; // 여기서 계산
}
  • lazyAdd: 두 숫자의 합을 계산하는 람다를 반환합니다. 이 람다는 호출될 때까지 실행되지 않습니다.
  • lazyResult(): 여기서 실제 계산이 수행됩니다.

익명 함수

람다 표현식을 사용하여 이름이 없는 함수를 정의하고, 이를 즉시 호출하거나 변수에 할당하여 사용할 수 있습니다.

auto printHello = []() { std::cout << "안녕하세요!" << std::endl; };
printHello();

printHello: “안녕하세요!”를 출력하는 람다 표현식을 할당받은 변수입니다. 이 변수를 통해 람다를 호출합니다.

클로저(Closure)

람다 표현식을 사용하여 클로저를 구현할 수 있습니다. 클로저는 람다가 정의된 범위의 변수들을 캡처하여 사용합니다.

int x = 10;
auto addX = [x](int a) { return a + x; };
std::cout << "결과: " << addX(5) << std::endl;
게임으로 배우는 C++, 생능출판
  • addX: 람다는 외부 변수 x를 캡처하고, 인자로 받은 a에 더하여 반환합니다. 이 예제는 람다가 클로저로서 작용하는 방식을 보여줍니다.

각 예제는 람다 표현식이 C++ 프로그래밍에서 다양한 상황에 유연하게 적용될 수 있음을 보여줍니다.

결론

람다 표현식은 C++에서 다양한 상황에서 코드를 간결하고 효율적으로 작성할 수 있게 해주는 강력한 기능입니다. 첫 번째 예제에서는 STL 알고리즘과 람다를 결합하여 컨테이너의 요소를 정렬, 출력, 변형하는 방법을 보여줍니다. 이를 통해 커스텀 조건에 따른 정렬이나 요소의 변환 등이 매우 간단해집니다. 두 번째 예제는 GUI 프로그래밍이나 이벤트 기반 시스템에서 이벤트 핸들러나 콜백 함수를 람다로 정의하는 방식을 소개합니다. 이는 코드의 가독성을 높이며, 이벤트 처리 로직을 직관적으로 표현할 수 있게 해줍니다. 세 번째 예제에서는 멀티스레딩 환경에서 std::thread와 람다를 사용하여 스레드를 생성하고 작업을 정의하는 방법을 보여줍니다. 네 번째 예제는 지연 실행(lazy evaluation)을 구현하는 방법으로, 리소스가 많이 소모되는 연산을 필요한 시점까지 미룰 수 있게 해줍니다. 다섯 번째 예제에서는 익명 함수의 정의와 사용을 보여주며, 마지막 예제는 클로저의 개념을 소개합니다. 이를 통해 람다가 정의된 범위의 변수들을 캡처하고 활용하는 방법을 설명합니다. 이 모든 예제들은 람다 표현식이 C++ 프로그래밍에서 얼마나 유연하고 강력한 도구인지를 잘 보여줍니다.