본문으로 바로가기

C++11) 람다식 (Lambda Expression) 과 std::function

category C++/Modern 2019. 2. 2. 04:11

<개요>


c++ 11부터 함수 객체를 조금 더 짧게 작성하자는 취지로 람다식이 등장했습니다.

람다식과 std::function에 대해 작성합니다.

만약 함수 객체 ( 단항 술어, 이항 술어 등 ) 을 모르신다면 STL 부분을 공부하시고 람다식을 공부하시는게 맞을 것 같습니다. 


<함수 객체(functor)>


일반적으로 remove_if 함수와 erase 함수를 사용해 vector 내의 모든 원소 중 짝수인 값만 삭제하고 싶다면, 이렇게 작성하실겁니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
 
class comp {
public:
    bool operator()(int x) {
        return ((x % 2== 0);
    }
};
int main() {
    vector<int> a = { 1,2,3,4,5,6,7,8,9,10 };
    comp mycomp;
    a.erase(std::remove_if(a.begin(), a.end(), mycomp));    
}
cs

(구조체로 작성할 수도있고, 함수포인터를 넘겨줄 수도 있습니다.)

일반적으로 x() 처럼 인자 없이 호출되는 함수 객체는, 발생자(Generator)
x(a) 처럼 1개의 인자를 가지고 호출되는 함수 객체는 단항 함수(Unary Fuction)
x(a,b) 처럼 2개의 인자를 가지고 호출되는 함수 객체는 이항 함수 (Binary Fuction) 이라고 합니다.

그외 bool 값을 반환형으로 가지는 함수 객체는 술어 라고 하는데, 
단항 함수(Unary Function)가 bool 값을 반환형으로 가질시엔 단항 술어
이항 함수(Binary Function)가 bool 값을 반환형으로 가직실엔 이항 술어 라고 합니다.


comp 클래스의 ()연산자를 오버로딩하여 remove_if의 술어로 사용하고 있습니다.
하지만 한번만 사용될 술어에 비해 너무 깁니다. 만약 저 comp 함수가 1~5번째 줄에 위치하고, remove_if 함수를 1000번째 줄에서 작성한다면
comp 함수가 도대체 뭘 하는 술어인지 위로 올려서 찾아가야 합니다.

<람다식 (Lambda Expression)>

방금 작성했던 comp 함수 객체를 람다식으로 옮겨보겠습니다.

1
2
3
4
5
6
7
 
int main() {
    vector<int> a = { 1,2,3,4,5,6,7,8,9,10 };
    a.erase(std::remove_if(a.begin(), a.end(), [](int i) {
        return ((i % 2== 0);
    }));    
}
cs

감이 좀 오시나요? 술어 대신 간단한 람다식 하나로 이것이 어떤 술어인지 확인할 필요도 없고 함수 객체를 만들 필요도 없게 되었습니다.


다음은 람다식의 사용 방법입니다.



[] = 변수 캡쳐

() = 인자 목록

-> ? = 리턴 타입 ( 생략할 수 있으나, 컴파일러가 자동으로 유추함 )

{ } 람다 몸체


입니다. 간단하게 예제를 하나 더 들어볼게요.


1
2
3
4
5
6
7
8
 
int main() {
    int value = 0;
    vector<int> a = { 1,2,3,4,5,6,7,8,9,10 };
    std::for_each(a.begin(), a.end(), [&value](int i) ->void {
        value += i;
    });
}
cs

위 람다는 벡터에 있는 값들을 value 에다가 저장시킵니다. 리턴값은 void ( 없음 ) 입니다.
만약 value를 출력하면 55가 나오겠지요.

1. 캡쳐식은 람다에서 선언하지 않은 변수, 그리고 자기와 똑같은 스코프에 있는 변수를 가져옵니다.
 ( 현재는 main 이라는 스코프 내에 value와 람다가 선언되어 있습니다. )
만약 &를 붙혀주지 않는다면, value는 const로 가져오게 되어 수정이 불가능 합니다.
다음은 캡쳐값에 대한 정보입니다:

[] : 아무것도 캡처하지 않음

[&x]: x만 Capture by reference 

[x] : x만 Capture by value

[&] : 모든 외부 변수를 Capture by reference

[=] : 모든 외부 변수를 Capture by value

[x,y] : x,y 를 Capture by value

[&x,y] : x는 Capture by reference , y는 Capture by value

[&x, &y] : x,y 를 Capture by reference

[&, y] : y 를 제외한 모든 값을 Capture by reference

[=, &x] : x 를 제외한 모든 값을 Capture by value


출처: https://vallista.tistory.com/entry/C-11-Lambda-Expression-람다-표현식-함수-객체-Functor [VallistA]




1
2
3
4
5
6
7
int main() {
    int value = 0;
    vector<int> a = { 1,2,3,4,5,6,7,8,9,10 };
    std::for_each(a.begin(), a.end(), [value](int i) ->void {
        value += i;    // 에러, value는 const 값입니다!!
    });
}
cs

2. 인자 목록은 a.begin()과 a.end() 사이의 원소들이 무엇이 넘어올것이냐 입니다.
현재는 int 형인 벡터 내에서 각 원소들을 넘겨주고 있으므로, int형이 됩니다.
만약 vector<pair<int,int>> 라면 인자 목록 타입은 pair<int,int> 가 될 것입니다.

3. 리턴 타입은 이 람다식이 술어의 리턴으로 무엇을 할 것이냐 입니다.
이 것은 생략해도 컴파일러가 자동으로 유추해주지만, 이상하게 유추될수도 있으므로 정수나 부동소수점을 사용할 땐 기입해주시는게 좋습니다.

4. 람다 몸체
이 술어가 어떤 일을 할 것이고, 그 술어가 무엇을 리턴할 것인지 캡쳐한 변수를 수정할 것인지 등등 행동을 작성하는 곳입니다.

람다는 흔히 이름 없는 함수 객체 라고도 하는데, 이름을 붙혀줄수도 있습니다.

1
2
3
4
 
    auto lambda = [](int i) ->int { return i; };    // 이렇게 이름을 붙혀줄수도있고
    [](int i) ->int {return i; }(3);    // 이렇게도 됩니다! 이 경우엔 3을 리턴하겠죠?

cs

<람다의 캡쳐식>

조금 더 자세하게 들어가 보겠습니다. 아마 람다식을 사용하시는 분들중께서는 전체를 값으로 캡쳐하시는 분도 계실겁니다.
1
2
3
{
    auto lambda = [=](int i) ->int { return i; };
}
cs

뭐 이건 작동을 잘 합니다. 하지만 위험 요소가 많습니다. 예를 한가지 들어 보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
using namespace std;
class Widget {
public:
    int i = 0;
public:
    Widget() { cout << "기본 생성자" << endl; }
    Widget(const Widget& rhs) { cout << "복사 생성자" << endl; }
};
int main() {
    Widget A;
    auto lambda = [=](int i) ->void { i = A.i; };
}
 
cs

이 경우엔 A를 만들기 위해 Widget의 기본 생성자가 호출될 것이고, 람다식에 값으로 캡쳐되면서 복사 생성자가 호출됩니다.
만약 람다식 밑에 저 람다식을 대입하는 식이 있다면 

1
2
3
 
    auto lambda2 = lambda;
 
cs

이 코드는 복사생성자를 한번 더 호출하게 될 것입니다.

<std::function>

std::function은 클로저 객체 ( 함수 객체, 함수 포인터, 람다 식)를 저장할 수 있는 기능입니다.
std::function은 C++11 이후, <function> 헤더를 include 하셔야 합니다.
그럼 간단한 예제 하나 보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <functional>
using namespace std;
int main() {
    std::function<bool(int)> functor;
    functor = [](int i) ->bool {
        return ((i % 2== 0);
    };
 
    cout << functor(1<< endl;        // false를 출력합니다.
    cout << functor(2<< endl;        // true를 출력합니다.
}
cs

std::function<리턴타입 (인자)> 형으로 사용합니다.
미리 선언만 해놓고, 나중에 정의하여 사용할 수 있으며 당장 정의해도 상관 없습니다.
auto와 같은것 같은데 왜 강력하냐구요? 순환 참조가 가능하기 때문입니다.
만약 func1 과 func2가 서로를 사용해야할때, auto는 미리 정의해두지 않으면 사용할 수 없기 때문입니다.

1
2
3
4
 
    auto func1 = [&func2]() {func2(); };    // error, func2가 선언되어 있지 않아요!
    auto func2 = [&func1]() {func1(); };    // ok, func1이 바로 위에 정의되어 있네요
 
cs

1
2
3
4
5
 
    std::function <void(void)> func1;
    std::function <void(void)> func2 = [&func1]()->void { func1(); };    // ok func1은 위에 선언 돼있어요
    func1 = [&func2]()->void {func2(); };    //ok func2는 위에 선언 및 정의 돼있어요
 
cs

그에 비해 std::function은 미리 functor를 선언만 해놓고 후에 정의할 수 있다는 장점이 있습니다. (물론 잘 쓰이진 않겠지만요...)

<마무리>

스탠다드에서는 람다를 사용을 하되 남발하지 말고, 꼭 한번만 쓰일 곳에 사용하라고 합니다.
만약 a와 b를 바꿔치기 (swap) 하는 람다를 만들었는데, 5~6줄 밑에 또 바꿔치기를 해야 할 필요성이 생긴다면 
람다를 만들지말고 함수를 하나 만드는게 낫다고 합니다.
꼭 유의하시기 바랍니다.

아 공부재밌네 다음은 STL 내용들좀 채워야 겠습니다.


'C++ > Modern' 카테고리의 다른 글

C++11) constexpr 키워드와 강한 열거형  (0) 2019.02.05
C++11) decltype (형식 지정자)  (1) 2019.02.03
C++11) std::forward (perfect forwarding)  (4) 2019.02.02
c++11) std::move (move semantics)  (2) 2019.02.01
C++11) auto 키워드  (0) 2019.01.30