본문 바로가기

진리는어디에/C++

[C++] 람다(lambda) 표현식 #1

들어가며

이번 포스트에서는 C++11 부터 추가된 람다 표현식에 대해 살펴 보도록하겠다. 람다 표현식(lambda expression)이란 '익명의 함수를 만드는 문법'으로써 정확하게는 익명의 함수 객체를 만드는 문법이다.

먼저 람다 표현식을 사용하여 vector를 정렬하는 간단한 아래 예제를 살펴 보자.

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

int main()
{
    std::vector<int> v = { 1, 3, 2, 5, 4 };
    
    std::sort(v.begin(), v.end(), [](int a, int b) -> int {
        return a < b;
    });

    for (int e : v)
    {
        std::cout << e << std::endl;
    }
    return 0;
}

9라인을 살펴 보면 vector의 정렬을 위해 사용된 sort 알고리즘을 사용하고 있다. sort 알고리즘의 세번째 인자는 정렬 비교를 위한 함수 객체 또는 함수 포인터를 넘겨 주게 된다. 위 예제에서는 별도의 함수나 함수 객체를 사용하지 않고 람다 표현식을 넘겨 주고 있다.

[](int a, int b) -> int {
    return a < b;
}

위 처럼 람다를 사용하면 간단 함수 포인터나 함수 객체가 필요한 곳에 일일이 함수를 만들어 줄 필요 없이 간단한 표현식으로 대체 할 수 있어 코드 가독성면이나 개발 속도 측면에서 이득이다.

이번 포스트에서는 이런 람다 표현식에 대해 자세히 살펴 보도록 한다.

람다 표현식의 문법

이전 섹션의 예제로 살펴 본 람다 표현식의 기본 형태는 아래와 같다. 

[](int a, int b) { return a < b; }

람다 표현식의 가장 특징적인 부분은 표현식 맨 앞의 대괄호 "[ ]"로 시작되는 람다 인트로듀서(lambda introducer)라는 것이다. 이는 람다 표현식이 여기서 부터 시작된다는 것을 알리는 표시 같은 것이며 람다 표현식의 나머지는 일반 함수와 동일하다.

람다 표현식을 좀 더 정석으로 표현하면 아래와 같다.

[captures] <template params> requires(optional) ( params
                                  spec  requires(optional) { body }

captures, template params, params와 같은 여러 알지 못하는 용어들이 나오지만 각 항목들을 살펴 보면서 다시 설명 예정이니 미리 겁먹거나 당황하지 말도록 하자. 이번 섹션에서 중요한 부분은 기본 람다 표현식의 형태를 눈에 익히는 것이다.

지역 변수 캡쳐(captures)

이번 섹션에서는 람다 표현식에서 지역 변수를 참조하거나 값을 변경할 수 있는 캡쳐(captures)에 대해 알아 보도록 한다.

#include <iostream>

int main()
{
    int v1 = 10, v2 = 20;
    
    auto f1 = [](int a) { return a + v1 + v2; };
    
    std::cout << f1(5) << std::endl;
    
    return  0;
}

위 람다 표현식에서는 람다의 body 부분에서 지역 변수 v1과 v2를 참조하고 있다. C#과 같은 다른 언어의 람다를 사용해본 경험이 있는 분이라면 위 코드가 정상적으로 동작할 것이라 생각하셨겠지만 C++에서는 위 코드는 "바깥쪽 함수의 지역 변수는 캡처 목록에 있지 않는 한 람다 본문에서 참조할 수 없습니다."와 같은 에러와 함께 컴파일 되지 않는다.

C++에서는 람다 표현식에서 외부 변수를 참조하기 위해서는 람다 인트로듀서(대괄호)에 참조하고자 하는 외부 변수들의 목록을 적어 주면 된다. 이를 변수를 캡쳐 한다고 한다. 이번 예제에서는 v1, v2가 필요하므로 코드는 아래처럼 변경 되면 된다.

auto f1 = [v1, v2](int a) { return a + v1 + v2; };

이번에는 아래와 같은 람다 표현식을 살펴 보자. 아래 코드는 참조하는 외부 변수를 람다 표현식 내부에서 값을 변경하려고 시도하고 있다.

auto f2 = [v1, v2](int a) { v2 = a + v1; };

위 코드는 "변경 불가능한 람다에서 복사 방식 캡처를 수정할 수 없습니다."라는 에러를 내면 컴파일에 실패한다. 이유는 우리는 v1과 v2를 캡쳐할 때 값에 의한 캡쳐를 했으며 이는 read only 값이기 때문에 변경이 불가능하다. 만일 람다 표현식 내부에서 값을 변경하고자 한다면 아래 처럼 변수 앞에 &를 추가하여 참조에 의한 캡쳐를 해야 한다.

auto f2 = [v1, &v2](int a) { v2 = a + v1; };
  형식 속성
[v1] capture by value read only
[&v1] capture by reference read/writge

람다 표현식의 리턴 타입

앞에서는 람다 표현식에서 외부 변수를 캡쳐하는 법을 배웠으니 이번에는 람다에서 결과를 리턴하는 방법을 살펴 보도록한다.

auto f1 = [](int a, int b) { return a + b; };

기본적으로 람다 표현식은 컴파일러가 리턴문으로 부터 리턴 타입을 추론하므로 명시적으로 리턴 타입을 지정하지 않아도 컴파일에 문제는 없다. 하지만 필자는 반환 타입 결정을 컴파일러 추론에 맡기지 말고 사용자가 직접 지정하는 것을 권장한다.

auto f1 = [](int a, int b) -> int { return a + b; };

이유는 가독성면에서도 명시적으로 타입이 지정되어 있는편이 읽기 편하고, 아래와 같이 리턴 타입이 명확하지 않은 리턴문에서 타입 추론에 실패하여 오류를 발생 시키기 때문이다.

auto f1 = [](int a, double b) -> double { if (a == 10) return a; return b; };

위 예제는 a의 값에 따라 int 타입을 리턴할 수도 있고 double 타입을 리턴할 수도 있으므로 컴파일러는 제대로된 리턴 타입을 추론하지 못하고 오류를 발생 시킨다. 이런 경우 '-> double'과 같이 람다가 리턴하는 타입에 대해 명시해주므로써 컴파일러가 반환 타입을 결정할 수 있도록 한다.

마치며

이상 람다 표현식의 기본적인 사양에 대해 살펴 보았다. 앞으로 다음 포스트(람다(lambda) 표현식 #2)에서는 람다의 좀 더 깊은 부분까지 살펴 보도록하겠다.

부록 1. 같이 읽으면 좋은 글

유익한 글이었다면 공감(❤) 버튼 꾹!! 추가 문의 사항은 댓글로!!