SW/C++

C++11 : 람다 함수에 대해 알아볼까요? (개념 및 예제)

얇은생각 2019. 1. 2. 07:30
반응형

람다 함수는 인라인 함수처럼 복잡한 함수 호출 과정을 생략해서 시간을 절약할 수 있을 뿐만 아니라, 코드의 가독성까지 높여줍니다. 람다 함수는 C++11부터 사용할 수 있으면 고급 함수이므로 처음 배울 때는 어려울 수 있으므로 반복 학습이 필요합니다. 


먼저 다음 예제를 살펴보겠습니다. 인라인과 람다 함수를 비교하였습니다. 코드가 다소 난해하다면 넘기고 읽으셔도 됩니다.


#include <iostream>
using namespace std;
 
class Chulsoo
{
public:
    void eat(int steakWeight);
    inline void eatInline(int steakWeight)
    {
        cout << "eatInline() :: 철수는 " << steakWeight << "g 짜리 스테이크를 먹는다" << endl;
    }
};
 
int main(void)
{
    Chulsoo chulsoo;
    chulsoo.eat(1000);
    chulsoo.eatInline(1000);
    [](int steakWeight){cout << "eatLambda() :: 철수는 " << steakWeight << "g 짜리 스테이크를 먹는다" << endl; }(1000);
    return 0;
}
 
void Chulsoo::eat(int steakWeight)
{
    cout << "eat() :: 철수는 " << steakWeight << "g 짜리 스테이크를 먹는다" << endl;
}
cs


현재 메인 함수에서 일반 함수와 인라인 함수 그리고 이번시간에 포스팅할 람다 함수가 선언되어 있습니다. 


실행 결과를 보면 함수 이름만 빼고 모두 같은 문자열을 출력한다는 것을 알 수 있습니다. 


아직 람다 함수의 문법을 배우지 않아 정확하게 모르지만, 일반 함수를 선언하고 정의하고 호출하는 패턴이 람다 함수에도 비슷하게 나타나는 것을 확인할 수 있습니다. 지금은 일반 함수와 람다 함수가 비슷한 패턴을 띄고 있다는 것만 이해하셔도 됩니다.


람다 함수의 문법을 배우기 전에 중요한 점이 있습니다. 바로 함수 이름이 없는 것입니다. 즉 람다 함수는 다음처럼 정의할 수 있습니다.


람다 함수란 인라인으로 정의와 호출을 한 번에 하는 익명 함수이다.


람다 함수의 문법은 다음과 같습니다.


[캡처] (매개변수 목록) mutable 예외 목록 -> 반환형 {함수몸체}


무언가 더럽지 않나요? 혼란을 없애기 위해 하나 하나씩 살펴보겠습니다.



[캡처]

캡처는 잡는다라는 의미를 가지고 있습니다. 즉 람다 함수 외부의 변수를 잡는 것입니다. 외부 변수를 잡는 이유는 람다 함수 내부에서 외부 변수에 접근하기 위해서 입니다.


함수를 호출할 때 값에 의한 호출과 참조에 의한 호출 방식이 있다고 배웠습니다. 같은 맥락으로 람다 함수 역시 외부 변수를 값으로 전달받아서 캡처하는 방식과 참조로 전달 받아서 캡처하는 방식이 있습니다.


[=] 모든 외부 변수를 값으로 전달 받아서 캡처합니다.

[&] 모든 외부 변수를 참조로 전달 받아서 캡처합니다.


#include <iostream>
using namespace std;
 
class Chulsoo
{
public:
    int count; // 철수가 스테이크를 먹는 횟수
};
 
int main(void)
{
    Chulsoo chulsoo;
    chulsoo.count = 1
    for (int i = 0; i < 10; i++)
    {
        [](int steakWeight){cout << "eatLambda() :: 철수는 " << chulsoo.count << "번째 " << steakWeight << "g 짜리 스테이크를 먹는다" << endl; }(1000);
    }
    return 0;
}
cs


왜 오류가 발생할까요?  캡처는 람다 함수 내부에서 외부 변수에 접근하기 위해서 사용합니다. 람다 함수내부에는 chulsoo.count라는 외부 변수가 있습니다. 하지만 예제에는 캡처 방식이 지정되어 있지 않고 공란입니다. 람다 함수 내부에서 어떠한 외부 변수에도 접근을 허용하지 않는 상황이므로 캡처할 수 없다는 오류가 발생합니다.


#include <iostream>
using namespace std;
 
class Chulsoo
{
public:
    int count; // 철수가 스테이크를 먹는 횟수
};
 
int main(void)
{
    Chulsoo chulsoo;
    chulsoo.count = 1
    for (int i = 0; i < 10; i++)
    {
        [=](int steakWeight){cout << "eatLambda() :: 철수는 " << chulsoo.count << "번째 " << steakWeight << "g 짜리 스테이크를 먹는다" << endl; }(1000);
    }
    return 0;
}
cs


수정한 예제를 보겠습니다. 차이점은 바로 [=] 입니다. 모든 변수를 값으로 전달받아서 캡처해서 값을 출력합니다. 


그런데 count 값이 변하지 않는 것을 확인할 수 있습니다. 왜 그럴까요?


#include <iostream>
using namespace std;
 
class Chulsoo
{
public:
    int count; // 철수가 스테이크를 먹는 횟수
};
 
int main(void)
{
    Chulsoo chulsoo;
    chulsoo.count = 1
    for (int i = 0; i < 10; i++)
    {
        [=](int steakWeight){cout << "eatLambda() :: 철수는 " << chulsoo.count++ << "번째 " << steakWeight << "g 짜리 스테이크를 먹는다" << endl; }(1000);
    }
    return 0;
}
cs


++ 연산자를 활용해서 count 값을 증가시켰습니다. 하지만 결과는 오류가 발생합니다. 결국 값으로 받았기 때문에 그 값을 수정할 수 없습니다. 


결국 람다 함수 내부에서 외부 변수를 수정할 수 있게 하려면 [&] 캡처를 사용해야 합니다. [&] 캡처는 모든 변수를 참조로 전달받아서 캡처하므로 람다 함수에서 외부 변수를 수정할 수 있습니다. 이제 우리가 원하는 대로 동작하는 람다 함수를 보겠습니다.


#include <iostream>
using namespace std;
 
class Chulsoo
{
public:
    int count; // 철수가 스테이크를 먹는 횟수
};
 
int main(void)
{
    Chulsoo chulsoo;
    chulsoo.count = 1
    for (int i = 0; i < 10; i++)
    {
        [&](int steakWeight){cout << "eatLambda() :: 철수는 " << chulsoo.count++ << "번째 " << steakWeight << "g 짜리 스테이크를 먹는다" << endl; }(1000);
    }
    return 0;
}
cs


이제 원하는 결과가 나오는 것을 확인하셨나요? 두가지 캡처를 이해했다면 절반 이상은 이해하신 것과 같습니다. 다른 캡처 종류에 대해 알아보겠습니다.


[] 람다 함수에서 외부 변수를 사용하지 않는다.

[a] 람다 함수의 외부 변수 a는 람다 함수 내에서 값으로 전달받아 사용한다. 다른 외부 변수는 람다 함수내에서 사용하지 않는다.

[&a] 람다 함수의 외부 변수 a는 람다 함수 내에서 참조로 전달 받아 사용한다. 다른 외부 변수는 람다 함수 내에서 사용하지 않는다.

[=, &a, &b] 기본적으로 람다 함수 외부의 모든 변수를 값으로 전달 받아 사용한다. 단 a, b 변수는 참조로 전달 받아 사용한다.

[&, a, b] 기본적으로 람다 함수 외부의 모든 변수를 참조로 전달받아 사용합니다. 단 a, b 변수는 값으로 전달받아 사용한다.


이제 다음으로 매개변수 목록에 대해 알아보겠습니다.



매개변수 목록


람다 함수에서 전달받을 매개변수 목록을 정의합니다. 매개 변수 목록은 캡처와 다르게 람다 함수 작성시 필수 요소가 아니므로 생략할 수 있습니다. 


#include <iostream>
using namespace std;
 
class Chulsoo
{
public:
    int count; // 철수가 스테이크를 먹는 횟수
};
 
int main(void)
{
    Chulsoo chulsoo;
    chulsoo.count = 1
    for (int i = 0; i < 10; i++)
    {
        [&]{cout << "eatLambda() :: 철수는 " << chulsoo.count++ << "번째 g 짜리 스테이크를 먹는다" << endl; }();
    }
    return 0;
}
cs



Mutable 속성


Mutable 속성은 앞서 캡처를 설명하면서 값에 의한 전달과 연관됩니다. 값으로 전달받아서 캡처하는 방식일 때, 한정자 const로 전달받으므로 수정할 수 없는 경우가 있습니다. 이것을 해결하려는 방법으로 mutable 속성을 사용할 수 있습니다. 


#include <iostream>
using namespace std;
 
class Chulsoo
{
public:
    int count; // 철수가 스테이크를 먹는 횟수
};
 
int main(void)
{
    Chulsoo chulsoo;
    chulsoo.count = 1
    for (int i = 0; i < 10; i++)
    {
        [=](int steakWeight)mutable{cout << "eatLambda() :: 철수는 " << chulsoo.count++ << "번째 " << steakWeight << "g 짜리 스테이크를 먹는다" << endl; }(1000);
    }
    return 0;
}
cs


람다 함수에 mutable 키워드로 속성을 추가하니 오류가 나지 않습니다. 즉 mutable 속성을 사용하면 값으로 전달받아서 캡처하는 람다 함수 안에서 외부 변수를 수정할 수 있씁니다. 하지만 값에 의한 전달이므로 값을 수정할 수는 없습니다. 따라서 실행결과는 계속해서 1번째로만 출력되는 것입니다. Mutalve도 매개변수 목록과 같이 필수가 아니고 생략할 수 있습니다.



예외목록


함수 몸체에서 발생할 수 있는 예외가 있을 수 있습니다. 개발을 하다 보면 이를 명시해야 하는 경우가 있죠. 예외에 대한 내용은 추후에 자세하게 다루도록 하겠습니다.



반환형


반환형은 일반함수로 따지면, 반환값의 자료형과 유사합니다. 즉 람다 함수는 일종의 함수이기 때문에 반환형을 명시해야합니다. 특별한 반환형을 지정하지 않고 특별한 return 문이 없으면 함수의 반환형은 void입니다.


반환형을 지정하는 형식은 다음과 같습니다.


->반환형


만일 직접 지정하지 않는다면 함수 몸체에서 return 문으로 반환하는 값의 자료형이 반환형이 됩니다. 다음 예제를 살펴보겠습니다.


#include <iostream>
using namespace std;
 
class Chulsoo
{
public:
    int count; // 철수가 스테이크를 먹는 횟수
};
 
int main(void)
{
    Chulsoo chulsoo;
    chulsoo.count = 1
    // 반환형을 char로 지정한 람다 함수
    cout << [=](int steakWeight)mutable->char{cout << "eatLambda() :: 철수는 " << chulsoo.count++ << "번째 " << steakWeight << "g 짜리 스테이크를 먹는다" << endlreturn steakWeight; }(67<< endl;
    // 반환형을 int로 지정한 람다 함수
    cout << [=](int steakWeight)mutable->int{cout << "eatLambda() :: 철수는 " << chulsoo.count++ << "번째 " << steakWeight << "g 짜리 스테이크를 먹는다" << endlreturn steakWeight; }(67<< endl;
    // 반환형을 지정하지 않았지만, steakWeight를 반환하므로 int
    cout << [=](int steakWeight)mutable{cout << "eatLambda() :: 철수는 " << chulsoo.count++ << "번째 " << steakWeight << "g 짜리 스테이크를 먹는다" << endlreturn steakWeight; }(67<< endl;
    return 0;
}
cs


실행해보셨나요? 67은 아스키 코드 값으로 C를 나타냅니다. 따라서 출력 결과를 보면 람다 함수가 반환하는 값은 정수 67이지만 문자 C를 출력합니다.  이전까지 잘 이해하셨다면 위 예제의 다른 람다 함수의 의미도 잘 이해하셨을 것입니다.



함수 몸체


함수 몸체는 함수의 정의부 입니다. 중괄호로 시작해서 끝나는 부분을 의미합니다.  함수 몸체는 캡처와 마찬가지로 람다 함수의 필수 구성 요소 입니다.


람다 함수는 인라인 함수의 장점을 그대로 이어받고, 바로 함수를 작성할 수 있는 장점이 추가 되어있습니다. 개발자 입장에서 코드를 더 간결하고 읽기 쉽게 작성할 수 있습니다. 


STL에서 람다 함수의 장점은 극대화됩니다. 표준 템플릿 라이브러리의 매개변수 등으로 사용할 수 있는 함수 포인터 대신 람다 함수를 적용해보면 람다 함수를 왜 사용하는 지 알 수 있습니다. 추후에 이 부분 역시 알아보도록 하겠습니다.

반응형