Language/C++

C++ 11 :: Lambda Expression (람다 표현식) + 함수 객체 (Functor)

VallistA2015. 2. 14. 17:42

C++ 11에 람다 표현식이 생겼다.

먼저, C++ 11에 추가된 람다가 뭐하는것인지 알아야한다.

대충 배경은 이렇다.. 

사람들은 언제나 조금이라도 불편한 것을 해결하려는 방향으로 일을 진행하는데.. 람다의 탄생 배경에는 함수 객체 (Function Object of Functor) 라고 불리는 개념이 연관되어 있다.

좀 더 쉽게 말하자면 함수 객체를 사용하는데 불편함을 느낀 사람들이 있어서 람다가 만들어졌다.

그리고 람다의 개념은 java나 c#등 타 언어에도 있을만큼 많이 쓰인다.


함수 객체는 C++ 11에 새롭게 추가된 기능이 아니라 기존부터 있었음. (람다가 추가되었다.)

함수 객체는 객체를 마치 함수처럼 사용하기에 붙여진 이름이고 함수가 되기위한 조건중 하나는 괄호인데, (함수 호출 연산자 Function call operator)를 사용해 파라미터 목록을 받는 것이다. 어떤 객체를 함수처럼 사용한다는 것은 객체에 괄호를 붙여서 마치 겉보기엔 함수를 호출하는 것처럼 사용한다는 의미이다.


하지만 아무 객체에나 괄호를 붙여준다고 컴파일러가 이를 이해를 하는가? 

컴파일러가 이해할 수 있게 하려면 클래스 안에서 함수호출 연산자를 오버로딩 해주어야 한다.


먼저 함수 객체의 개념을 알도록 하자 (Functor)


1.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <functional>
#include <string>
#include <vector>
#include <array>
 
class Functor
{
public:
     void operator()()
     {
          std::cout << "Simple Functor" << std::endl;
     }
};
 
int main(void)
{
    Functor func;
    func();                                // func뒤에 함수 호출 연산자를 붙임 func 변수는 분명히 Functor 클래스의 객체일 뿐이지만, 뒤에 함수 호출 연산자인 괄호를 붙여주고나니 함수처럼 보임.
    return 0;
}


2.


구조체는 모든게 public인 C++ 클래스로 간주 가능하다. ( 구조체를 사용하여 인터페이스 만들기도 한다. )

함수객체용 클래스 만드는 방법은 의외로 간단한데, 클래스를 정의할 때 내부에 함수 호출 연산자만 오버로드 해서 정의를 해주면 되기 때문이다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
#include <functional>
#include <string>
#include <vector>
#include <array>
 
struct Accumulator
{
    Accumulator()
    {
         counter = 0;
    }
     
    int counter;
    int operator()(int i)   // 함수 객체를 만들기 위해서는 함수 호출 연산자를 반드시 오버로딩 해야한다.
    {
        return counter += i;
    }
};
 
int main(void)
{
    Accumulator a;                      // Accumulator 함수 객체 클래스로부터 객체 a를 선언한다.
    std::cout << a(10) << std::endl; // 객체 a에 10을 넣어준 후 값을 출력하면 10이 나온다.
    std::cout << a(20) << std::endl;
 
     // 계속 누적되서 값이 들어간다.
     // 일반 함수는 어떤 파라미터를 받던간에 연산을 수행후 해당 결과를 반환하고 종료된다. 예전에 실행했던 어떤 연산의 결과도 기억을 하지 못한다.
     // 함수 객체는 자기가 만들어진 상태를 해당 객체가 소멸되기 전까지 계속해 기억하게 된다.
 
    return 0;
}


함수 객체를 왜 알아야 하는가를 적자면.

STL의 많은 알고리즘이 함수 객체를 즐겨 사용을 하기 때문에 더더욱 알아야 하며

이게 과연 특징이라고 볼 수 있냐는 의견도 있지만, 분명 STL 알고리즘에서는 함수 객체를 많이 사용하기 때문에, 적어도 알아야 할 개념이다.

먼저 STL에서 정의하는 함수 객체 타입에 대해 이해를 해야하는데, STL에서는 함수 객체를 세가지 타입으로 분류를 하는데, 이들 각각에 다른 이름을 사용한다. 

foo() 함수처럼 인자 없이 호출되는 함수객체는 발생자(Generator) 라고 부르고

foo(x) 처럼 하나의 인자를 받는 것을 단항 함수 (Unary Function) 이라고 부른다.

foo(x,y)처럼 두 개의 인자를 받는 것을 이항 함수 (Binary Function) 이라고 부른다.

인자의 개수와는 별도로 bool 값을 반환하는 파라미터가 함수 포인터나 함수 객체라면 술어 (Predicate) 라고 부른다.

단항 함수가 불 값을 반환하는 경우는 단항 술어 (Unary Predicate) 또는 술어 (Predicate) 라는 이름으로 부르고, 이항 함수가 불 값을 반환하는경우엔 이상 술어 (Binary Predicate) 라고 부른다.

세개 이상은 STL에서 존재하지 않기 때문에 그런말은 사용하지 않는다.

우리가 알아야 할 것은 단항 술어 (Unary Predicate) 이항 술어 (Binary Predicate) 이 두가지 경우만 알아두면 된다.


3.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>
#include <functional>
#include <string>
#include <vector>
#include <array>
 
class EvenOddFunctor
{
public:
     EvenOddFunctor() : evenSum(0), oddSum(0) {}
 
     void operator() (int x)
     {
          if(x%2 == 0)
              evenSum += x;
          else
              oddSum += x;
     }
     int sumEven() const {return evenSum;}
     int sumOdd() const {return oddSum;}
 
private:
     int evenSum;
     int oddSum;
};
 
int main(void)
{
    EvenOddFunctor functor;
    std::array<int, 10> theList = {1,2,3,4,5,6,7,8,9,10}; // Uniform Initialization 으로 초기화
    functor = std::for_each(theList.cbegin(), theList.cend(), functor); // cBegin(처음) 부터 cEnd(마지막) 까지 체크해서 functor(opeartor()(int x)) 에 넘김 그러면 각각의 숫자를 넣은 operator 함수의 값을 체크해서 functor (EvenOddFunctor)을 넘겨줌. 그러면 현재 EvenOddFunctor인 functor에 넣어줌으로써 evenSum, oddSum등의 변수를 저장하게 된다.
     
    std::cout << "Sum of evens: " << functor.sumEven() << std::endl;
    std::cout << "Sum of odds: " << functor.sumOdd() << std::endl;
 
    getchar();
    return 0;
}

4.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>
#include <functional>
#include <string>
#include <vector>
#include <array>
 
bool IsOdd(int i)
{
    return ((i%2) == 1);
}
 
class CIsOdd
{
public:
    bool operator() (int i)
    {
        return ((i%2) == 1);
    }
};
 
// #define _USE_FUNC_POINTER_ // 주석 풀면 함수 포인터, 주석 걸면 함수 객체
 
int main(int argc, char** argv)
{
    std::vector<int> v = {10, 25, 40 , 55};               // Uniform Initialization으로 초기화
 
    #ifdef _USE_FUNC_POINTER_
        auto it = find_if(v.begin(), v.end(), &IsOdd);  // 함수 포인터를 사용한 예로 IsOdd() 함수 타입의 포인터를 세번째 파라미터로 제공해줌 IsOdd() 함수 이름 자체가 함수가 위치한 메모리 가리킴 IsOdd라고만 써도됨 &IsOdd해도됨
    #else
        // 함수 객체 예에서는 일단 함수 객체인 CIsOdd 클래스의 객체를 하나 생성한 후 이를 전달
        // 예제 에서는 양쪽 모두 실행 결과는 같고, 어느쪽을 사용해도 됨. 하지만 함수 포인터와 함수 객체 모두 장단점이 있음.
 
        // 함수 포인터는 간결하게 작성할 수 있지만 함수 객체는 함수 객체용 클래스를 정의해야 하고, 함수 호출 연산자를 오버로딩 해주어야 하기 때문에 코딩하기 번거로움
        // 함수 포인터는 간단하게 작성할 수 있지만 전역 함수일 가능성이 높음. 간단하게 사용한다는 측면에서 전역 함수로 선언하는 것이 맞지만, 한두 개가 아니라 개수가 늘어난다면 네임스페이스를 어지럽히는 주범이 될 수도 있습니다.
        // 함수 객체는 앞에서 말한대로 상태 정보를 가질 수 있기 때문에, 함수 포인터용 일반 함수와 비교했을 때 사용자화도 편리
        // 실행 측면에서 보면 함수 포인터는 컴파일러가 어떤 함수를 사용해야 할지 컴파일 할 때 알 수 없고, 런타임이 되어야 실제 사용할 함수가 무엇인지 알 수 있습니다. 이에 반해 함수 객체는 컴파일러가 컴파일할 때
        // 실제 사용할 함수가 무엇인지 알 수 있기 때문에 함수 객체를 사용하면 컴파일러에 의해 인라인 대상으로 선정될 수 있음. 컴파일러에 의해 인라인 후보로 선정될 수 있다는 것은 성능상의 이점을 노려볼 수 있다는 말임.
        CIsOdd objIsOdd;
        auto it = find_if(v.cbegin(), v.cend(), objIsOdd);
    #endif
        std::cout << "The first odd value is " << *it << std::endl;
    return 0;
}


람다

함수 객체를 설명한 이유는 함수 객체가 람다와 목적이 동일하면서도, 이를 한층 업그레이드 한 개념이기 때문이었다.

함수 객체와 비교했을 때 람다가 갖는 장점 가운데 두 가지만 꼽아본다면 첫 번재는 직접 코드를 개발하는 입장에서 코딩이 간편해진다는 점, 두번째는 다른 사람이 작성한 코드를 읽을 때 코드의 가독성이 향상된다는 것이다.

굳이 두가지 장점으로 나누었지만 곰곰이 생각해보면 이 두가지 장점은 내가 작성한 간결한 코드는 남이 읽기도 쉽다는 하나의 사실로 귀결된다.

프로그래머는 자신이 작성한 코드만 읽는 사람이 아니라 다른 사람이 작성한 코드도 항상 마주해야하는 직업이므로 이는 아주 중요한 장점이 된다.


자, 람다 예제를 보도록 하자.


1.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
#include <functional>
#include <string>
#include <vector>
#include <array>
 
int main(int argc, char** argv)
{
    std::vector<int> v = {1, 2, 3, 4, 5};
 
    // 람다 사용 X
    std::cout << "람다 사용 X" << std::endl;
     
    for (auto it = v.begin(); it != v.end(); it++)
    {
        std::cout << *it << std::endl;
    }
 
    std::cout << std::endl;
 
    // 람다 사용 O
    std::cout << "람다 사용 O" << std::endl;
 
    std::for_each(v.begin(), v.end(), [](int val)               // [] 부분 람다 함수의 시작점을 알리는 부분
    {                                                           //
        std::cout << val << std::endl;                          //  대괄호 사이가 람다 함수의 구체적인 본체
    });                                                         //
 
    return 0;
}


2.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>
#include <functional>
#include <string>
#include <vector>
#include <array>
 
class CIsOdd
{
public:
    bool operator()(int i)
    {
        return ((i%2) == 1);
    }
};
 
// No. 1 ( Non Lambda )
int main(int argc, char** argv)
{
    std::vector<int> v = {10, 25, 40, 55};
 
    CIsOdd objIsOdd;
 
    //std::find_if 는 stl을 돌면서 해당 객체를 탐색 ( 3번째 인자 함수 객체 )
    auto it = std::find_if(v.cbegin(), v.cend(), objIsOdd);
 
    std::cout << "[Using Functor] : The first odd value is " << *it << std::endl;
 
    return 0;
}
 
// No. 2 ( Use Lambda )
int main(int argc, char** argv)
{
    std::vector<int> v = {10, 25, 40, 55};
 
    auto it2 = std::find_if(v.cbegin(), v.cend(), [](int i) -> bool {
        return (i%2) == 1;
    });
 
    std::cout << "[Using Lambda] : The first odd value is " << *it2 << std::endl;
 
    return 0;
}

3.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <iostream>
#include <functional>
#include <string>
#include <vector>
#include <array>
 
class EvenOddFunctor
{
public:
    EvenOddFunctor() : evenSum(0), oddSum(0) {}
 
    void operator() (int x)
    {
        if (x%2 == 0)
            evenSum += x;
        else
            oddSum += x;
    }
    int sumEven() const {return evenSum;}
    int sumOdd() const {return oddSum;}
 
private:
    int evenSum;
    int oddSum;
};
 
int main(int argc, char** argv)
{
    EvenOddFunctor functor;
    std::array<int, 10> theList = {1,2,3,4,5,6,7,8,9,10};                     // Uniform Initialization으로 초기화
 
    int evenSum = 0;
    int oddSum = 0;
    std::for_each(theList.cbegin(), theList.cend(), [&](int i)
    {
        if (i%2 == 0)
            evenSum += i;
        else
            oddSum += i;
    });                                                                         // cBegin(처음) 부터 cEnd(마지막) 까지 체크해서 functor(opeator()(int x)) 에 넘김
                                                                                // 그럼 각각의 숫자를 넣은 operator 함수의 값을 체크해서 functor (EvenOddFunctor) 을 넘겨줌.
                                                                                // 그러면 현재 EvenOddFunctor인 functor에 넣어줌으로써 evenSum, oddSum등의 변수를 저장하게 됨
 
    std::cout << "Sum of evens: " << evenSum << std::endl;
    std::cout << "Sum of odds: " << oddSum << std::endl;
 
    getchar();
    return 0;
}


대충 람다 표현식을 보아봤는데, 이제 다시 설명에 들어가도록 하겠다.


람다의 사용법은 간단하다. 

- []{} 

- [](인자){}


람다 소개자 : [] (Lambda Introducer)

파라미터 지정자 : () (Parameter Specifier)

람다 몸체 : {} (Lambda Body)


이 두가지 방법중 하나를 사용하면 되는데, 람다 표현식에 변수를 넘기고 싶은 경우에는 가운데 괄호를 넣고 인자를 넣으면 된다.


람다 함수는 람다 소개자로 시작해야 인식을 하는데, 람다 소개자라는 것은 바로 맨 처음 대괄호를 지칭한고 파라미터 지정자는 람다 함수에 넘겨줄 인자를 입력할 수 있는 메서드다. 그리고 람다 몸체는 일반 함수와 쓰임새가 같으며, {} 내부에 람다 함수가 수행할 일을 작성한다.


그리고 맨 뒤에 ()를 붙이고 인자를 넣어주면 함수가 시작이 된다.


4.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <functional>
#include <string>
#include <vector>
#include <array>
 
int main(int argc, char** argv)
{
    [](void){std::cout << "살고싶다" << std::endl;}();
 
    [](std::string name){std::cout << "Hello " << name.c_str() << std::endl; }("World!");
 
    bool bChk = [](int a) -> bool{       // 명시적으로 제시를 해줘야 컴파일러가 제대로 암 만약 이렇게 안해주면 컴파일러가 자기 맘대로 설정
        if (a%2 == 0)
            return false;
         
        return true;
    }(1);
 
    std::cout << bChk << std::endl;
 
    return 0;
}


[] 는 캡처 절 이라고도 하는데, 캡처의 사전적 의미를 찾아보면, "Take into one's possession or control by forse" 라고 되어있다.

남의 것을 자기 것으로 만들거나 마음대로 조종하는 것을 뜻한다. 람다 함수에서 캡처의 대상은 람다 함수 외부에 선언된 (하지만 람다 함수를 감싸는 범위안의) 변수 이다.


5.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
#include <functional>
#include <string>
#include <vector>
#include <array>
 
int main(int argc, char** argv)
{
    std::array<int, 10> theList = {1,2,3,4,5,6,7,8,9,10};                
 
    int evenSum = 0;
    int oddSum = 0;
 
    std::for_each(theList.cbegin(), theList.cend(), [&](int i)
    {
        if (i%2 == 0)
            evenSum += i;
        else
            oddSum += i;
    });                                                                    
                                                                             
                                                                             
 
    std::cout << "Sum of evens: " << evenSum << std::endl;
    std::cout << "Sum of odds: " << oddSum << std::endl;
 
    getchar();
    return 0;
}


[&] 라는 것이 보일 것 이다. 이것은 람다 외부에서 선언된 모든 변수를 람다 함수 내부에서 레퍼런스 타입으로 캡처 (Capture by reference) 해 사용할 수 있도록 하고 있다. (즉 람다 함수 외부에서 선언된 변수를 가져다 쓸 수 있다는 것이다)


그러므로 이 두 변수를 람다 함수 내부에서 따로 정의하지 않았지만 사용할 수 있었던 것이다.

또한 레퍼런스 타입으로 캡처했기 때문에 람다 내부에서 작성한 변경 사항은 그대로 원래 변수 값으로 반영된다.


레퍼런스 타입으로 캡처하는 방법 외에 값으로 캡처 (Caputre by value) 할 수도 있다.

외부 변수를 간단ㅁ히 넘겨봐야 이를 적용한 연산만 수행할 때는 값으로 캡처하는 편이 좋다. 단 이때는 &를 붙이지 않는 상태로 작성한다. 가령 Caputre By Value로 넘겨 받으려고 하면. [&] 가 아닌 [evenSum, oddSum] 으로 해도 된다.


다만 밑의 구문에서는 Reference로 받아야 값 연산이 가능해지므로 (주소 값 직접 조작) & 이 옳다.


다음은 속성 값을 알아보도록 하자.


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

[&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


static 변수는 캡처할 수 없고 Automatic storage duration을 갖는 일반변수만 가능하다.


값으로 캡쳐된 외부 변수는 const의 특성을 가지게 되어, 람다 함수 몸체에서 값에대한 수정이 불가능해진다.

그러나 mutable 이라는 키워드 (mutable은 storage class specifier 스토리지 클래스 지정자) 를 이용하여 수정이 가능하다.


6.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <functional>
#include <string>
#include <vector>
#include <array>
 
int main(int argc, char** argv)
{
    int a = 0;
 
    [=]() mutable
    {
        a++;
        std::cout << "람다 내부 : " << a << std::endl;
    }();       
 
    // mutable로 바꿔주었다 한들 결국은 값만 바꿧으므로 외부의 값까지 바뀌진 않음
    std::cout << "람다 외부 : " << a << std::endl;
 
    return 0;
}


람다 함수는 auto 키워드를 이용해 함수 자페를 하나의 변수에 할당할 수 있고, 또한 람다 함수 내부에서 또 다른 람다 함수를 호출 할 수도 있다.


7.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <iostream>
#include <functional>
#include <string>
#include <vector>
#include <array>
 
int main(int argc, char** argv)
{
    std::array<int, 10> arr = { 3, 2, 1, 4, 9, 10, 7, 8, 4, 6 };
 
    auto ShowOriginalArray = [=]()
    {
        std::for_each(arr.cbegin(), arr.cend(), [](int i)       // 람다가 함수 객체라는 증거
        {
            std::cout << i << ",";
        });
 
        std::cout << std::endl;
    };
 
    ShowOriginalArray();
 
    std::sort(arr.begin(), arr.end(), std::less<int>());
 
    auto ShowSortedArray = [=]()
    {
        std::for_each(arr.cbegin(), arr.cend(), [](int n)       // 람다가 함수 객체라는 증거
        {
            std::cout << n << ",";
        });
         
        std::cout << std::endl;
    };
 
    ShowSortedArray();                                          // 람다 함수에서 &로 받지 않고 = 값만 받았으므로 함수 내부에서만 계산
 
    ShowOriginalArray();                                        // 다음에 적용되지 않음
 
    return 0;
}


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

C++ 11 :: R-Value Reference  (0) 2015.02.14
C++ 11 :: static_assert Keyword  (0) 2015.02.14
C++ 11 :: Smart Pointer (Shared_ptr, Unique_ptr, Weak_ptr)  (3) 2015.02.14
C++ 11 :: Array  (0) 2015.02.13
C++ 11 :: decltype  (0) 2014.10.20

댓글

VallistA

병특이 끝나서 게임에서 웹으로 스위칭한 프로그래머.
프로그래밍 정보등을 공유합니다.
현재는 이 블로그를 운영하지 않습니다.
vallista.kr 로 와주시면 감사하겠습니다!

자고 싶습니다. ㅠㅠ

Github      :: 링크

궁금한점 문의 주시면 답변드리도록 하겠습니다

VISITED

Today :

Total :

SNS

  • 페이스북아이콘
  • 카카오톡아이콘
  • 트위터아이콘

Lately Post

Lately Comment