티스토리 툴바


토픽

New South Wales의 범죄현황 리포트를 Google Maps 에 매쉬업해주는 장난감을 한번 만들어 봤습니다.


개발툴

- Eclipse

- Python 2.6

- Google Maps API ver 3

- Javascript


스크린 샷



접속 URL

http://nswcrime.alwaysdata.net/crimewatch/crimenews.html

무료 웹 호스팅하는 곳인데, 계약 위반으로 언제 짤릴지 모른다는...ㅠㅠ 얼렁 괜찮은 유료 서비스 하나 가입하겠네요. 지도 데이터는 1시간마다 업데이트 됩니다.


후기

자바스크립트가 생각보다 강력한 언어라는 생각이 들었습니다. 십 몇년 전에 갖고 놀던 그 언어가 아닌 듯 합니다. @.@ 구글 맵스 팀이 왜 자바스크립트를 선택했는지 이제 좀 이해가 갈락 말락.

파이썬은 역시 이런 초간단/초스피드 어플리케이션 개발에 쵝오. 


Further works

코드 정리좀 해서 웹에도 올리고, 기회가 되면 안드로이드 플랫폼으로 포팅도 한번 해봐야 겠습니다.

저작자 표시 비영리 변경 금지
Posted by In search of dream 꿈찾아고고씽

이번 포스트에서는 Perfect forwarding 에 대해 알아봅시다. 

우리말로는 완전 포워딩? 완벽 포워딩 이정도 되겠습니다만, 한글로 번역하니 역시 어색한 감이 있네요. 


C++ Forwarding Problem


일단 아래 코드를 보시죠.

    1 #include <iostream>

    2 

    3 void Foo(int& i)

    4 {

    5     std::cout<<"Passed value: "<<i<<std::endl;

    6 }

    7 

    8 void ForwardThis(int& i)

    9 {

   10     Foo(i);

   11 }

   12 

   13 int main()

   14 {

   15     int a = 1;

   16     ForwardThis(a);

   17     return 0;

   18 }


실행 결과는 아래와 같습니다.

Passed value: 1

계속하려면 아무 키나 누르십시오 . . .


아무런 문제없이 실행됩니다.

main 함수 내부 코드를 보면 라인 16에서 ForwardThis 라는 함수를 호출하고 a 라는 변수를 넘겨주고  있습니다. a 라는 변수는 1이라는 상수값(r-value)을 가지고 있는 L-Value 입니다. 따라서 ForwardThis 라는 함수가 받아야 할 파라미터 형식인 레퍼런스 타입의 정수형으로 변환될 수 있습니다. 

C++ 11 표준 어법으로 좀 더 정확히 말해봅시다. 

ForwardThis 함수가 받아야할 파라미터 형식은 L-Value 레퍼런스 타입입니다. a는 lvalue 이므로 당연히 ForwardThis 함수에 파라미터로 넘겨줄 수 있습니다.


이제 main 함수 내부에 한줄 추가해봅시다. (라인 17 입니다)

   13 int main()

   14 {

   15     int a = 1;

   16     ForwardThis(a);

   17     ForwardThis(1);

   18     return 0;

   19 }

   20 


의미상으로 볼 땐 같습니다. 16f라인에서는 a 라는 변수(L-Value) 를 통해 1을 넘겨주고 있지만, 17라인에서는 바로 1 (R-Value) 을 넘겨주고 있습니다. 

위 코드는 컴파일러에서 다음과 같은 에러를 냅니다.

error C2664: 'ForwardThis' : cannot convert parameter 1 from 'int' to 'int &

int 를 int& 로 바꿀 수 없다는 불평을 하고 있습니다.


R-Value Reference 관련 포스트에서 언급한 대로 L-Value 레퍼런스는 그 우측값으로 L-Value 만 받을 수 있습니다. 1은 L-Value 가 아닌 R-Value 입니다. 그렇기 때문에 컴파일러는 에러를 내는 겁니다.


함수 오버로딩을 이용한 Forwarding 문제 해결


이 문제를 해결하기 위해서는, ForwardThis 함수와 Foo 함수의 시그내쳐를 변경해서 overload 된 함수를 제공해주면 됩니다. 아래처럼요.


    1 #include <iostream>

    2 void Foo(int& i)

    3 {

    4     std::cout<<"Passed value: "<<i<<std::endl;

    5 }

    6 void Foo(const int& i)

    7 {

    8     std::cout<<"Passed value: "<<i<<std::endl;

    9 }

   10 void ForwardThis(int& i)

   11 {

   12     Foo(i);

   13 }

   14 void ForwardThis(const int& i)

   15 {

   16     Foo(i);

   17 }

   18 

   19 int main()

   20 {

   21     int a = 1;//ok

   22     ForwardThis(a);//ok

   23     ForwardThis(1); //ok

   24     return 0;

   25 }


실행결과입니다.


Passed value: 1

Passed value: 1

계속하려면 아무 키나 누르십시오 . . .


이제는 에러가 없네요. 근데, 왜 에러가 없죠? ^^ 

ForwardThis(a); 를 호출한 라인 22는 라인 10과 2의 함수들을 호출하지만, ForwardThis(1); 은 라인 14 와 6의 함수를 호출하기 때문입니다.

정수형 타입의 a 변수에 1을 저장해서 넘겨주는 것과 1을 바로 넘겨주는 것. 

어차피 같은 거 아닐까요? 의미상으론 같지만, 컴파일러 내부적으로는 엄격하게 L-Value 와 R-Value, 정확히 말하면, 해당 expression의 constness 를 체크하고 있었던 것입니다. 


그냥 이렇게 쓰면 되는거 아닌가요? 그때 그때 필요한 파라미터 타입을 오버로딩해서 쓰면 되잖아요?

맞습니다. 그래도 됩니다.


하지만, 함수 파라미터가 하나가 아니라면 어떨까요? 두 개라면? 세 개라면?  

게다가 함수 호출하는 부분에서 ForwardThis(1,a,3,...)  이런 식으로 호출한다면요? 

가능한 조합을 남김없이 찾아내서 하나 하나, 꼼꼼하게, 디테일하게, 오버로딩 해줘야 됩니다. 

생각만해도 끔찍하지 않습니까?


"난 단지 파라미터를 포워딩해주고 싶었을 뿐이라고요" 라고 컴파일러에게 항변하고 싶지만, C++의 현재 구현으로는 간단히 해결할 수 없는, 이런 문제가 있었던 겁니다.


R-Value Reference를 이용한 Perfect Forwarding


이제 C++11에서 새롭게 도입된 rvalue reference 를 이용해 문제를 좀더 간단히 해결할 수 있게 되었습니다.

바로 ForwardThis 함수의 파라미터 타입을 L-Value 레퍼런스인 int& 에서 R-Value 레퍼런스인 int&& 로 바꿔주기만 하면  됩니다.


   46 void ForwardThis(int&& i)

   47 {

   48     Foo(i);

   49 }

   50 


어떻게 이게 가능할까요. 

C++11이 나오기 전에는, 레퍼런스 변수에 대해 다시 레퍼런스 변수로 지정하는 것은 에러였습니다.

그러나, C++11 에서는 다음과 같은 Reference Collapsing Rule 을 도입했습니다.


  1. T&의 &     ==> T&    (L-Value 레퍼런스의 L-Value 레퍼런스는 L-Value 레퍼런스)
  2. T&의 &&   ==> T&    (L-Value 레퍼런스의 R-Value 레퍼런스는 L-Value 레퍼런스)
  3. T&&의 &   ==> T&    (R-Value 레퍼런스의 L-Value 레퍼런스는 L-Value 레퍼런스)
  4. T&&의 && ==> T&&  (R-Value 레퍼런스의 R-Value 레퍼런스는 R-Value 레퍼런스)


따라서, 

1. ForwardThis(int&& i) 함수가 L-Value 타입의 인자 i에 대해 호출되면, 위의 룰 # 2에 의해 i는 L-Value 로 추론됩니다.

2. ForwardThis(int&& i) 함수가 R-Value 타입의 인자 i에 대해 호출되면, i는 자연스럽게 R-Value 로 추론됩니다. 


좀 복잡하긴 한데, 일단 이렇게 이해하고 넘어가야겠습니다. 저도 @.@

  

그런데, 여기서 VS2010 베타버전을 사용하시는 분들이 잠깐 알아두셔야 할 게 있습니다.

C++11 으로 확정되기 전, C++0x 표준에서는 위의 main 함수내 코드를 하나도 바꾸지 않아도 됬었습니다.

따라서, C++0x 표준을 준수한 Visual Studio 2010 Beta 버전에서는 아래코드가 아무 이상없이 동작했습니다.


    1 #include <iostream>

    2 void Foo(int& i)

    3 {

    4     std::cout<<"Passed value: "<<i<<std::endl;

    5 }

    6 void Foo(const int& i)

    7 {

    8     std::cout<<"Passed value: "<<i<<std::endl;

    9 }

   10 //void ForwardThis(int& i)

   11 //{

   12 //    Foo(i);

   13 //}

   14 //void ForwardThis(const int& i)

   15 //{

   16 //    Foo(i);

   17 //}

   18 

   19 void ForwardThis(int&& i)

   20 {

   21     Foo(i);

   22 }

   23 

   24 int main()

   25 {

   26     int a = 1;//ok

   27     ForwardThis(a);//error C2664

   28     ForwardThis(1); //ok

   29     return 0;

   30 }


하지만, C++11 로 확정되면서 R-Value 레퍼런스와 관련된 작은 변화가 있었고, C++11 표준을 따른 VS2010 정식 버전에서는, 위의 27 라인에서 아래과 같은 컴파일 에러가 납니다.

error C2664: 'ForwardThis' : cannot convert parameter 1 from 'int' to 'int &&'


이 컴파일 에러를 해결하기 위해서는 std::move 함수를 사용해야 합니다. 

ForwardThis(a); 

라인을

ForwardThis(std::move(a)); 

로 바꿔주면 컴파일 에러는 해결됩니다.



std::move


위에서 언급된 std::move 함수에 대해 짤막하게 짚고 넘어갑시다.

std::move는 C++11 에서 새롭게 도입된 함수입니다. move semantics를 지원하기 위해 등장했고, 파라미터로 받는 객체의 내용을 전달해줄 함수로 복사가 아닌 이동!시켜주기 위한 함수입니다.

이를 위해 std::move는 인자로 받은 변수를 rvalue로 바꿔줍니다. 함수 내부를 보면 단지 R-Value 로 static type cast 하도록 구현 되어있습니다.

이 함수는 utility 헤더에 정의되있습니다.

저작자 표시 비영리 변경 금지
Posted by In search of dream 꿈찾아고고씽
TAG C++, C++11

이번에는 C++11 의 move 생성자와 move 할당연산자에 대해 알아봅시다.

C++11 에서는 move 생성자와 move 할당 연산자를 제공합니다.

move 생성자와 move 할당 연산자의 원리는 Copy 생성자와 copy 할당연산자의 원리와 유사합니다.

자신과 동일한 타입의 객체를 인자로 받아서 새롭게 생성한 인스턴스에 그 내용을 이전합니다.

다만 차이점은 Copy 의 경우 멤버변수를 위한 메모리를 할당했어야 했는데, move 에서는 그걸 하지 않아도 된다는 것입니다.


Copy constructor

먼저 C++11 이 나오기 전에 하던대로 해봅시다. 

간단히 배열을 제공하는 클래스의 예입니다. 복사생성자만 제공해줬습니다.


    1 class MyArray

    2 {

    3 public:

    4     MyArray(int size) : m_pArr(new int[size]), m_size(size) {}

    5 

    6     MyArray(const MyArray& other) : m_pArr(new int[other.m_size]), m_size(other.m_size)

    7     {

    8         for(int i = 0; i < m_size; i++)

    9         {

   10             m_pArr[i] = other.m_pArr[i];

   11         }

   12     }

   13 

   14     ~MyArray()

   15     {

   16         delete [] m_pArr;

   17     }

   18 

   19 

   20 private:

   21     int* m_pArr;

   22     int m_size;

   23 

   24 };

   25 


역시 복사 생성자 내부에서 새롭게 메모리를 할당한 후, 멤버 간의 복사가 이루어지고 있습니다. 
별 무리 없는 클래스입니다.


Move constructor

위의 클래스를 C++11 에서 지원하는 move 생성자(내부적으로 r-Value 레퍼런스를 이용한) 를 이용하여 내부적으로 복사없이, 인스턴스의 리소스 소유권 이전을 간단하게 해결할 수 있습니다. move 생성자를 추가한 아래 코드를 보세요.

    1 #include <iostream>

    2 

    3 class MyArray

    4 {

    5 public:

    6     MyArray(int size) : m_pArr(new int[size]), m_size(size)

    7     {

    8         for(int i = 0; i < m_size; i++)

    9             m_pArr[i] = i;

   10     }

   11     //Copy constructor

   12     MyArray(const MyArray& other) : m_pArr(new int[other.m_size]), m_size(other.m_size)

   13     {

   14         for(int i = 0; i < m_size; i++)

   15             m_pArr[i] = other.m_pArr[i];

   16     }

   17 

   18     //Move constructor

   19     MyArray(MyArray&& other) : m_pArr(other.m_pArr), m_size(other.m_size)

   20     {

   21         other.m_pArr = NULL;

   22     }

   23     ~MyArray()

   24     {

   25         delete [] m_pArr;

   26     }

   27 

   28 

   29 private:

   30     int* m_pArr;

   31     int m_size;

   32 

   33 };


move 생성자는 훨씬 간단하네요. 새롭게 메모리를 할당할 필요도 없고요. 

우리가 move 생성자에서 눈여겨 봐야 할 것이 두 가지 있습니다.

1. 바디 내에서 other.m_pArr 을 NULL 로 셋팅하고 있습니다.

2. 파라미터가 const 타입이 아닙니다.

먼저 첫번째로 other.m_pArr 을 NULL 로 셋팅하는 이유를 봅시다. other 객체도 언젠가는 (scope 을 벗어날때) 소멸되는 임시 객체입니다. 이 객체가 소멸하는 순간 당연히 소멸자가 불립니다. 소멸자를 보시면 m_pArr 을 삭제하는 루틴이 있는 것을 알 수 있습니다.

move semantics에서 m_pArr 은 이미 다른 인스턴스에게 소유권이 이전된 메모리 영역입니다. 따라서 other.m_pArr 을 NULL 로 셋팅해주지 않는다면 다른 인스턴스 (this) 에게 이미 소유권이 넘어간 m_pArr을 delete 하게되고, 이는 당연히 move를 호출한 해당 인스턴스 (this) 가 가지고 있는 m_pArr을 삭제하는 결과를 초래하게 됩니다. 이는 마치 deep-copy 가 아닌 shallow-copy 와 마찬가지 상황인 것입니다. 따라서 other.m_pArr 을 NULL 로 셋팅해주는 이유는, 소멸자에서 delete[] m_pArr 이 호출되는 것을 방지하기 위함입니다.

두번째로 파라미터가 const 타입이 아닌 것은 너무나 당연합니다. 내부에서 other 의 멤버인 m_pArr을 수정해야 하니까 말이죠. const 면 건드릴 수 조차 없겠죠?


Performance

이제, move constructor 의 수행성능을 한번 봅시다.


   35 #include <ctime>

   36 

   37 int main()

   38 {

   39     MyArray a(100000000);

   40 

   41 

   42     //Copy constructor

   43     clock_t ct1 = clock();

   44     MyArray myCopy(a);

   45     clock_t ct2 = clock();

   46     double msec1 = 1000.0 * (ct2-ct1) / CLOCKS_PER_SEC;

   47     std::cout<<"Copy time: "<<msec1<<" msec"<<std::endl;

   48 

   49 

   50     //Move constructor

   51     clock_t mt1 = clock();

   52     MyArray myMove(std::move(a));

   53     clock_t mt2 = clock();

   54     double msec2 = 1000.0 * (mt2-mt1) / CLOCKS_PER_SEC;

   55     std::cout<<"Move time: "<<msec2<<" msec"<<std::endl;

   56 

   57     return 0;

   58 }



   59 /*

   60 Output:

   61 Copy time: 343 msec

   62 Move time: 0 msec

   63 계속하려면 아무 키나 누르십시오 . . .

   64 */

유의미한 값을 얻기 위해 1억개의 정수배열을 가지고 테스트해보았습니다만, 그 차이가 실로 엄청나네요.

move 의 경우 거의 시간이 걸리지 않는 것을 알 수 있습니다.


유사한 다른 예제코드도 한번 보죠.

내부적으로 vector 를 멤버로 가지고 있는 클래스에서 각각 copy constructor 와 move constructor 를 이용했을때의 속도차를 살펴보았습니다. VC++10.0 (VS2010) 에 포함된 C++ 라이브러리는 STL에 이미 move semantics가 구현되있다는 것을 잊지마세요. 

이말은 STL 컨테이너인 vector에도 move constructor 나 move assignment operator 가 구현되어있다는 의미입니다.


    1 #include <vector>

    2 #include <ctime>

    3 #include <iostream>

    4 class X

    5 {

    6 private:

    7     std::vector<int> m_vec;

    8 

    9 public:

   10     X() : m_vec(100000)

   11     {       

   12         for(size_t i=0; i < 100000; ++i)

   13             m_vec[i] = i;

   14     }

   15 

   16     //copy constructor

   17     X(const X& other) : m_vec(other.m_vec){}

   18 

   19     //move constructor

   20     X(X&& other) : m_vec(std::move(other.m_vec)){}

   21 

   22     X& operator=(const X& other)

   23     {

   24         m_vec = other.m_vec;

   25         return *this;

   26     }

   27 

   28     X& operator=(X&& other)

   29     {

   30         m_vec = std::move(other.m_vec);

   31         return *this;

   32     }

   33 };

   34 

   35 int main()

   36 {   

   37     X x1;

   38 

   39     //check copy time

   40     clock_t ct1 = clock();

   41     X x2(x1);

   42     clock_t ct2 = clock();

   43     double msec = 1000.0 * (ct2-ct1) / CLOCKS_PER_SEC;

   44     std::cout<<"Copy time: "<<msec<<" msec"<<std::endl;

   45 

   46     //check move time

   47     clock_t mt1 = clock();

   48     X x3(std::move(x1));

   49     clock_t mt2 = clock();

   50     double msec2 = 1000.0 * (mt2-mt1) / CLOCKS_PER_SEC;

   51     std::cout<<"Move time: "<<msec2<<" msec"<<std::endl;

   52 

   53     return 0;

   54 }

   55 



   56 /*

   57 output:

   58 

   59 Copy time: 46 msec

   60 Move time: 0 msec

   61 

   62 */

역시 굉장한 차이를 보여주고 있고, 특히 move constructor 의 경우 거의 시간이 걸리지 않는 다는 것을 다시 한번 알 수 있습니다.


저작자 표시 비영리 변경 금지
Posted by In search of dream 꿈찾아고고씽
TAG C++, C++11

이번 포스트는 move semantics 에 대한 첫번째 포스트입니다.

이 글에서는 move semantics에 대해 이야기 하기 전에, C++ 복사(copy)가 가진, C++ copy semantics가 가지고 있는 문제점에 대해서 우선 생각해봅시다. 왜냐면 move semantics 가 등장한 배경에는 C++ copy 에 대한 고민이 있었기 때문입니다. 문제점을 인식하지 못한다면, 왜 이걸 해야하는지 동의하기도 힘드니까요.

C++ 에서는 어느 한 객체의 상태를 다른 객체로 복사하는 일이 가능합니다. 

C++ 에서 복사와 관련된 메커니즘은 Copy Constructor 와 Assignment Operator 에 의해 처리됩니다. 이 둘은 프로그래머가 정의할 수도 있고, 컴파일러에 의해 정의되기도 합니다. 만약 컴파일러가 클래스 내에서 이 둘을 찾을 수 없다면 자동적으로 이를 생성해 주도록 되어있기 때문입니다. 하지만, 클래스 내부에서 다른 타입의 리소스를 가지고 뭔가 한다면, deep copy를 위해 프로그래머가 직접 정의한 copy con 과 asgn oprt 가 있어야 하는 것이 best practice 입니다.


이 둘의 전형적인 구현 패턴은 매우 유사합니다.


1. 멤버 변수를 위해 새로 리소스를 할당해 주고

2. 인자로부터 this 포인터로 복사를 실행하고

3. 기존 리소스를 해제한 후

4. 해제된 기존 리소스에 새로운 리소스를 할당하여 이를 반환합니다.


이는 마치 디카에 있는 SD 메모리카드에서 컴퓨터로 사진을 옮기는 시나리오와 비슷합니다. 


1. 우선 사진 복사를 위해 PC 에 폴더를 하나 만듭니다. 

2. 이제 SD 카드에서 사진들을 선택해 컴퓨터로 복사합니다. 

3. 복사가 끝나면, SD 카드의 해당 원본 사진들을 삭제하기 시작합니다. 

4. 삭제가 끝나면, 이제 PC에서 복사한 사진을 가지고 작업을 시작합니다.


이 글을 읽으시는 분 들 가운데 설마 이렇게 하는 분들은 없으시겠죠? ^^ 

하지만 놀랍게도, C++ 의 메커니즘과 위의 비효율적인 디카 사진 옮기기 시나리오는 거의 동일합니다.


Q) 그럼, 디카 사진은 어떻게 옮겨야 됩니까? 

A) 그냥 ctrl + c, ctrl + v 하면 됩니다. 


Q) 그럼 C++ 에서는요?

A) 그냥 닥치고 하던 대로 하세요.  ㅠ,.ㅠ


우리는 여태까지 C++ 세상에서 이렇게 비효율적으로 살아왔다는 불편한 진실. ^^;;


이러한 C++ 언어의 기능(제한)적인 측면은 STL의 구현에도 지대한 영향을 미쳤더랬습니다.

가령 예를 들어, vector 안에 어떤 객체를 넣는 오퍼레이션을 생각해보죠.


    1 #include <vector>

    2 class MyClass;

    3 

    4 void foo()

    5 {

    6     MyClass o;

    7     std::vector<MyClass> vec;

    8 //    ...

    9 //    ... vec에 대한 뭔가 많은 오퍼레이션 ...

   10 //    ...

   11     vec.push_back(o);

   12 }

   13 

   14 


만약 라인8 부터 10까지의, 뭔가 많은 오퍼레이션으로 인해, vec.push_back(o); 라인을 만나기 전에 vec이 모두 채워져 버렸다고 가정해봅시다. vector는 동적으로 증가하는 배열입니다. 그렇기 때문에 벡터가 꽉 차버리는 경우, 컴파일러 내부적으로 아래와 같은 일들이 벌어집니다.


1. 크기가 더 큰 새로운 vector 를 만들고 (STL 구현 벤더에 따라 다릅니다만, 보통 1.5 ~ 2 배 씩 그 크기가 증가하는 것으로 알려져 있습니다.) 

2. 이곳에 기존벡터에 있던 엘리먼트들을 모두 복사합니다. 

3. 복사가 모두 끝나면, 기존 벡터안에 있던 엘리먼트들을 모두 파괴시킵니다. 벡터 내부 엘리먼트가 객체인 경우엔 객체의 소멸자를 호출합니다.

4. 기존 벡터 자체를 파괴시킵니다.

5. 새로운 벡터의 이름을 기존 벡터 이름으로 바꿔치기 합니다.


위의 모든 과정이 끝나고 나서야, 이제 vec.push_back(o); 가 실행됩니다. 


프로그래머: "전 그냥 벡터 끄트머리에 객체 2개만 더 추가하고 싶었을 뿐이라고요..."

컴파일러: "닥치세요. 2개던 100개던 상관없어. 난 저렇게 밖에 할 줄 모르거든!"



다른 예로, 벡터를 반환하는 팩토리 함수도 한번 생각해봅시다.


    1 #include <vector>

    2 class MyClass;

    3 typedef std::vector<MyClass> MyVec;

    4 

    5 MyVec createMyVec(); // 팩토리 함수

    6 void foo()

    7 {

    8     MyVec vec;

    9     //...

   10     //... vec을 이용한 다양한 오퍼레이션 ...

   11     //...

   12     vec = createMyVec();

   13 }


8 라인이 실행되면, 우선 STL 구현에 따라 정의된 디폴트 사이즈로 vector가 생성됩니다. 다음에 vec을 이용해서 뭔가 다양한 일들을 수행합니다. 그리고, 12 라인이 실행되면 다음과 같은 일이 벌어집니다.


1. createMyVec() 은 내부에서 임시로 비어있는 MyVec 객체를 하나 만들고 (vec2 라고 합시다), 

2. createMyVec() 함수 안에서 정의된 일을 수행한 후, 

3. vec2를 vec 에 대입합니다.


3번 과정의 내부를 좀 더 들여다 봅시다.

3-1) createMyVec() 함수가 반환할 때 우선 넘겨받아야 할 vec 에 빈 자리가 있어야 하므로, 

3-2) vec이 가지고 있던 엘리먼트들을 파괴합니다. vec 안에 무엇이 들어있던 간에 파괴됩니다.  

3-3) vec 내부 확보가 끝나면, vec2 내의 엘리먼트를 vec 으로 하나 하나 복사합니다.

3-4) 복사를 모두 마치면, vec2는 소멸되어야 하므로, vec2 내부에 들어있는 엘리먼트를 파괴합니다. 

3-5) vec2 의 엘리먼트들이 모두 소멸되고 난 후에, vec2를 소멸합니다


이제 어느 정도 C++ Copy Semantics 가 가지고 있는 문제점에 대해 인식이 되셨나요?

C++11 에서는 copy의 이러한 비효율을 없애기 위해 move semantics 라는 개념을 들고 나왔고, 더불어 C++ STL 에도 이런 메커니즘을 대거 적용했습니다.

이로 인해, 심지어! 기존 레거시 코드의 퍼포먼스도 향상되었고요. 

이런 일이 가능했던 것은 모두 다 r-value 레퍼런스라는 명품조연 덕분입니다. 

다음 포스트에는 move semantics 가 어떻게 생겼는지 알아보도록 하겠습니다.

저작자 표시 비영리 변경 금지
Posted by In search of dream 꿈찾아고고씽
TAG C++, C++11

이번 포스트에서는 R-Value 레퍼런스에대해 알아보도록 하겠습니다. R-Value 에 대해 이야기하기 전에 먼저 기존 C++의 레퍼런스 타입에 대해 잠깐 리마인드 해보고 넘어갑시다.

    1 MyClass a;

    2 

    3 MyClass& b = a;

이미 알고 계시듯이 a 라고 객체를 하나 만들었고, b 라는 레퍼런스 타입을 하나 만들어서 a를 참조하도록 하고 있습니다. 이때 b 를 레퍼런스 밸류라고 불러왔었습니다.

하지만, C++11이 나온 이제부터는 b 값의 타입을 레퍼런스라고 부르지 마세요. 대신 L-Value 레퍼런스라고 불러야 합니다. 이게 다 R-Value 레퍼런스가 나온 덕분입니다. 

새로운 C++ 표준에서 레퍼런스라는 개념은 L-Value 레퍼런스와 R-Value 레퍼런스로 나뉘어집니다. 이제부터 여러분의 팀원이 뭐시기 뭐시기는 레퍼런스 타입이라고 하면 이제부터는 L-Value 인지 R-Value 인지 다시 한번 물어보도록 합시다. 

그럼 R-Value 레퍼런스는 얼마나 이쁘게 생겼는지 한번 봅시다.

    1 MyClass a;

    2 

    3 MyClass&& c = a;


위 코드에서 c가 R-Value 레퍼런스 타입 변수입니다. 이게 뭔가요. &&를 붙이는 것만 빼고는 똑같군요. 

흠. 좀실망인데요.

하지만, 맞습니다. R-Value 레퍼런스는 L-Value 레퍼런스랑 한 가지만 빼고 똑같습니다. ( 아, &를 하나 더 붙이는거까지 합치면 두 개군요.)

L-Value 레퍼런스는 그 우측값(r-value)으로 L-Value 만 받을 수 있습니다. 그에 반해 R-Value 레퍼런스는 그 우측값(r-value) 로 L-Value 뿐만 아니라 R-Value 도 받을 수 있습니다. 

헷갈리나요? 헷갈리는 분들은 L-Value 와 R-Value 에 대한 정의가 명확하시지 않으셔서 그럴겁니다.  L-Value/R-Value 가 뭐였는지 다시 한번 되집어 볼까요? 

C 표준에서는 이 둘을 아래와 같이 정의합니다.

An L-Value is an expression e that may appear on the left or on the right hand side of an assignment, whereas an R-Value is an expression that can only appear on the right hand side of an assignment

해석하면, 대입식 (assignment) 에서 왼쪽 또는 오른쪽에 나올 수 있는게 L-Value (left-hand side value의 약자), 오른쪽에만 나올 수 있는게 R-Value (right-hand side value의 약자) 입니다. 뭔가 성의없어 보이는 설명에 더 헷갈리기만 하는군요. 어쩔 수 없네요. 예를 들어 봅시다.

    1 int a = 13;

    2 

    3 int b = 12;

a 와 b는 L-Value 이고, 13 과 12는 R-Value 입니다. L-Value는 오른쪽에도 나올 수 있다고 했습니다. 즉, L-Value 가 R-Value가 필요한 자리에 나오면, L-Value는 묵시/자동적으로 R-Value 로 변경됩니다. 따라서

    5     a = b; //ok

    6     b = a; // ok

    7     a = a*b; //ok

는 모두 가능합니다.

위에서 a*b 는 R-Value 입니다. 따라서 ,

    9     int c = a*b; // ok

   10     //a*b = 12; // error. R-Value must be on the right side.


앞에서 R-Value가 필요한 자리에 L-Value가 나오면, 묵시/자동적으로 R-Value 로 변경된다고 했는데요. R-Value는 그게 안됩니다. R-Value는 무조건 R-Value 자리에 있어야 됩니다. R-Value 는 대입식의 오른쪽이 지정석입니다. 지정석 외에 다른 자리에 앉으면 안됩니다.

또한 위 코드에서 모든 변수들에 const 를 붙여주지 않았기 때문에, L-Value 는 처음 정의가 된 이후에, R-Value를 통해 변경이 가능합니다. 이에 반해 R-Value 는 변경이 되지 않습니다. R-Value 는 굳이 const로 지정해주지 않아도 const 의 성격을 가지고 있습니다.

C의 L-Value / R-Value 정의는 C++ 에서도 여전히 유효합니다만, 이렇게 이해하는 것이 더 좋을겁니다.

특정 메모리 위치를 가르키고 있으며, 어떤 값을 assign 할 수 있고, & 오퍼레이터를 통해 해당 메모리의 주소값을 가져올 수 있는 expression 이 L-Value 입니다. R-Value는 L-Value 가 아닌 expression 입니다. 

즉, 값을 담을 수 없으며 (can't assign) , L-Value 에 값을 담기 위해 임시(temporary)로 생성된 expression 입니다. 값을 담을 수 없기 때문에 당연히 & 오퍼레이터를 써서 그 내부의 값을 알아오는 것도 불가능합니다.

하나 더. 

C++에서 레퍼런스 타입을 반환하는 함수에 대한 호출은 L-Value를 반환하고, 이를 제외한 함수는 모두 R-Value를 반환합니다. 즉,

   12     int& foo();

   13     foo() = 12; // ok

   14     int* p1 = &foo(); // ok

foo() 함수 호출은 L-Value를 반환하고, L-Value 는 & 연산자를 통해 주소값을 가져올 수 있습니다.

   16     int bar();

   17     int j = 0;

   18     j = bar(); // ok. bar() is R-Value

   19 //    int* p2 = &bar(); // error. R-Value can't be applied & operator.

이제 R-Value 와 L-Value의 차이가 이해가 되시나요?


자. 그럼, R-Value 레퍼런스. 도.대.체 이걸 왜 알아야 되는 걸까요. 이게 왜 필요한 걸요? 이런 간략한 변화따위로 우리 디벨로퍼들 인생을 편하게 해줄 수 있을까요?

그 이유는 R-Value 레퍼런스를 사용해야 C++11에 새롭게 도입된 move semantics 를 구현할 수 있었기 때문입니다. 또한 R-Value 레퍼런스는 역시 C++11에 새롭게 등장한 perfect forwarding 또한 해결할 수 있는 키를 쥐고 있는 개념입니다. 무대의 여배우를 화려하게 빛나게 해주기 위한 명품조연이라고나 할까요.

L-Value 와 R-Value의 차이를 정확히 이해하고 있어야, 앞으로 등장할 move semantics 와 perfect forwarding의 개념을 쉽게 이해할 수 있습니다. 그 중에서도 move semantics 은 정말 C++ 11에서 람다함수만큼이나 천지가 개벽할 만한 내용입니다. 물론 이걸 안다고 해서 하늘에서 돈다발이 쏟아져 내리는 건 아닙니다만, 엄청나게 훌륭한 개념이긴 합니다. C++ 코드의 성능도 좋아지고, 남들 앞에서 조금 아는척도 하실 수 있을거고요. ^^

암튼, R-Value와 L-Value 가 조금 헷갈리더라도 정확히 알고 넘어가야 합니다.

이제까지의 내용을 정리해서 코드로 만들어봤습니다. 설명은 코드 안에 커멘트형식으로 달아두었습니다. 우스운 영어실력이긴하지만, ^^ 이해하기 어렵지는 않으실거예요.

    1 #include <iostream>

    2 

    3 int main(int argc, char** argv)

    4 {

    5     using namespace std;

    6 

    7     //L-Value Reference you're familiar since C++98

    8     int a = 1;

    9     //int& r = 3; //error as L-Value reference can only have

   10                     // L-Value as its R-Value

   11     int& ra = a;

   12 

   13     cout<<"Original: L-Value(a): "<<a<< ", L-Value Reference(ra): "<<ra<<endl;

   14     ra = 2;

   15     cout<<"Modified: L-Value(a): "<<a<< ", L-Value Reference(ra): "<<ra<<endl;

   16 

   17     //newly introduced R-Value Reference in C++11

   18     int b = 10;

   19     int&& rb = 20; //no error as R-Value reference can have

   20                     // either L-Value or R-value as its R-Value

   21 

   22     cout<<"Original: L-Value(b): "<<b<< ", R-Value Reference(rb): "<<rb<<endl;

   23     rb = b;//Now R-value reference is assigned by L-Value

   24     cout<<"Modified: L-Value(b): "<<b<< ", R-Value Reference(rb): "<<rb<<endl;

   25 

   26     return 0;

   27 }

실행결과입니다.


R-Value 레퍼런스를 활용한 move semantics 와 perfect forwarding 에 대한 개념은 이어지는 포스트에서 다루겠습니다. 이제까지는 밑밥이었습니다. 본격적으로 재밌는 이야기는 다음편에 ^^;;

저작자 표시 비영리 변경 금지
Posted by In search of dream 꿈찾아고고씽
TAG C++, C++11

이번 포스트에서는 C++11에 새롭게 도입된 static_assert라는 키워드에 대해 알아봅니다. 먼저 이 키워드의 용도는 컴파일 타임에 어떤 주어진 조건을 검사하는데 사용합니다. Syntax 는 아래와 같습니다.

static_assert(expression, message)

expression 이 true 로 판명되면, 컴파일러는 아무것도 안합니다. false 로 판명되면 C2338 에러를 뿜고, 두번째 파라미터로 넘겨준 message를 출력합니다.

    1 #include <iostream>

    2 

    3 int main()

    4 {

    5     const auto MAX_LEVEL = 120;

    6     static_assert(MAX_LEVEL <= 100, "Warning - Max Level"); //error C2338: Warning - Max Level

    7     return 0;

    8 }

    9 

문법과 사용법 자체는 크게 어려운게 없습니다. 

활용예를 생각해봅시다.

멀티 플랫폼을 위한 어플리케이션 작성에 활용해 볼 수 있겠습니다.

    1 #include <iostream>

    2 

    3 int main()

    4 {

    5     //...

    6     static_assert(sizeof(void*) == 4, "This code is not supported for 64bit");

    7     //...

    8     return 0;

    9 }

포인터의 크기는 32 비트 머신에서는 4이지만, 64비트 머신에서는 8입니다. 플랫폼의 크기에 따라 다른거죠.

위 코드는 만약 32 비트 머신에서 컴파일하면 아무런 에러가 없지만, 64비트 머신에서 컴파일하면 마찬가지로 C2338 에러를 냅니다. 이 코드는 해당 코드의 빌드 머신 플랫폼을 체크할때 사용할 수 있겠네요.


static_assert 는 클래스 내에 삽입할 수도 있습니다. 가령 아래와 같은 템플릿 코드 내에서 static_assert를 삽입하여, 입력되는 값을 컨트롤 할 수도 있겠습니다.

    1 #include <iostream>

    2 

    3 template <int N>

    4 struct MyStruct

    5 {

    6     static_assert(N < 10, "MyStruct requires N < 10");

    7 };

    8 

    9 int main()

   10 {

   11     MyStruct<1> m1;

   12     MyStruct<5> m2;

   13     MyStruct<9> m3;

   14     MyStruct<13> m4; // error C2338: MyStruct requires N < 10

   15 

   16     return 0;

   17 }



다른 예로, 여러분이 라이브러리 개발자라고 해봅시다. 예전 버전에서 A라는 클래스를 쓰고 있었는데, 문제가 발견되어 A 클래스를 A2 클래스로 바꿨습니다. 예전 버전과의 호환성 문제때문에 A 클래스는 지워버릴 수가 없는 상황입니다.

다른 팀에 있는 개발자들에게 회의나 이메일 등을 통해 "A 클래스는 예전 버전을 위한 코드이니, 내일부터는 A2 클래스를 써주세요" 라고 알려줬지만, 죽어라고 말 안듣고 계속 A 클래스를 사용하고 있어서 그로 인한 문제가 QA 로부터 계속해서 터져나오는 상황입니다.

이럴 때는 A 클래스 내에 static_assert를 삽입하여, 코드 내부에서 좀더 명확히 해줄 수도 있겠습니다.

    1 class A

    2 {

    3 public:

    4     A(){};

    5     ~A(){};

    6     static_assert(0, "don't use this class A - deprecated. Use A2 instead");

    7     // ...

    8 };

    9 

   10 class A2

   11 {

   12 public:

   13     A2(){}

   14     ~A2(){}

   15     // ...

   16 };

   17 

   18 void Test()

   19 {

   20     A a;//error C2338: don't use this class A - deprecated. Use A2 instead

   21     //...

   22 }

   23 

   24 int main()

   25 {

   26     //...

   27     Test();

   28     // ...

   29     return 0;

   30 }

라인 6을 보면, 예전 클래스인 A 클래스 내부에, static_assert 를 삽입하고, 파라미터로 조건은 무조건 false 가 리턴되도록 0을 주고, 적절한 메시지를 줬습니다. 

18 라인의 Test() 라는 함수에서 또 A 클래스를 쓰려고 시도합니다만, 이제는 C2338 에러를 내면서 A2 클래스를 써야 한다고 메시지 창에 보여줍니다. 

저작자 표시 비영리 변경 금지
Posted by In search of dream 꿈찾아고고씽
TAG C++, C++11

이번 포스트는 람다에 대한 마지막 포스트로 람다에 대한 다른 설명 대신, C++ 구루이자 ISO C++ 표준위원회 위원장님이신 Herb sutter의 Lambda 함수 관련 페이퍼 가운데 일부분을 옮겨봅니다. 

예제 중 어느 코드가 가장 맘에 드시나요? 어느 코드가 가장 읽기 편하고, 의미가 명확하게 와 닿나요?


1. 리스트 내부 돌기

//Hand-written loop:

for(auto i = strings.begin(); i != strings.end(); ++i ) 

cout << *i << endl; 

}


//STL without lambdas:

copy( strings.begin(), strings.end(), ostream_iterator<string>(cout, “\n”) );


//STL with lambdas:

for_each( strings.begin(), strings.end(), []( string& s ) { cout << s << endl; } );


2.리스트 내부를 돌면서 뭔가 실행

//Hand-written loop:

for( auto i = strings.begin(); i != strings.end(); ++i ) 

 // 뭔가 많은 일을 하는 코드

}


//STL without lambdas:

for_each( strings.begin(), strings.end(), LoopBodyFunctor(…) );

// 어딘가에는 반드시 LoopBodyFunctor 를 구현해야 되는 불편이 있습니다.


//STL with lambdas:

for_each( strings.begin(), strings.end(), []( string& s ) 

 // 뭔가 많은 일을 하는 코드

} );

// 코드 그 자체로 좀 더 명확하게 하는 일을 설명합니다. 



3. 벡터에서 x보다는 크고, y 보다는 작은 첫번째 요소 찾기

//Hand-written loop:

auto i = v.begin(); // because we need to use i later

for( ; i != v.end(); ++i ) 

    if (*i > x && *i < y) 

        break; 

}


//STL without lambdas (C++0x):

auto i = find_if( v.begin(), v.end(), bind( logical_and<bool>(), bind(greater<int>(), _1, x), bind(less<int>(), _1, y) ) );


//STL with lambdas:

auto i = find_if( v.begin(), v.end(), [=](int i) { return i > x && i < y; } );


4. 정수가 들어있는 벡터에서 합과 곱 구하기

//Hand-written loop:

int sum = 0; 

long long product = 1;

for( auto i = values.begin(); i != values.end(); ++i ) 

    sum += *i; 

    product *= *i; 

}


//STL without lambdas:

int sum = accumulate( c.begin(), c.end(), 0 );

long long product = accumulate( c.begin(), c.end(), 1, multiplies<int>() );


//Drawbacks: Need to use/write functor. 

//Multi-pass is slower. 

//It can also trash cache locality. (And did you notice how easily we lost “long long”?)


//STL with lambdas:

int sum = 0; long long product = 1;

for_each( values.begin(), values.end(), [&]( int i ) { sum += i; product *= i; } );



5. 배열 안에서 모든 요소에 특정 함수 (Foo()) 적용하기.

//Hand-Written loop

void DoFoo( float a[], int n ) 

{

    for( int i = 0; i < n; ++i ) 

    {

        Foo( a[i] ); 

    }

}


//STL with lambdas

void DoFoo( float a[], int n ) 

{

    for_each( &a[0], &a[0]+n, []( float f ) { Foo( f ); } );

}


역시 람다는 STL에 적용해야 제맛인 듯 합니다.

전체 자료는 첨부파일로 링크합니다. 

한번 읽어보세요. 재미있습니다.


저작자 표시 비영리 변경 금지
Posted by In search of dream 꿈찾아고고씽
TAG C++, C++11

이번 포스팅에서는 람다 함수를 어떻게 만들어야 하는지 알아보겠습니다.

아래는 세상에서 가장 간단한 람다함수입니다.

[]{};

희한하긴 한데, 좀 싱겁네요. 하나 하나 뜯어보도록 하겠습니다.


1. 람다 함수 원형

[]{};

위의 코드는 아무 일도 하지 않는 람다함수입니다. 하지만 문법상으로는 아무 오류가 없습니다. 일반함수로 치면 void foo(){} 와 동일합니다.


[] : Lambda Introducer 라고 부릅니다. 무조건 이걸로 시작해야 C++11 컴파일러가 알아먹습니다.

{} : Lambda Body 라고 부릅니다. 일반 함수처럼 이 내부에 람다함수가 할 일을 적어넣어야 합니다.


2. 람다 함수 바디

이번엔 람다 바디를 한번 채워보겠습니다.

[]{std::cout<<"Hello World"<<std::endl;};

람다의 헬로월드 격인 예제입니다. 일반 함수로 치면, void foo() { std::cout<<"Hello World"<<std::endl;} 와 동일합니다.


위의 람다 함수는 아래처럼 바꿔쓸 수 있습니다.

[](){std::cout<<"Hello World"<<std::endl;};


아래처럼 써도 됩니다.

[](void){std::cout<<"Hello World"<<std::endl;};


뭐가 달라졌는지 눈치 채셨나요? Lambda introducer 뒤에 () 를 추가했습니다. () 는 람다함수로 파라미터를 넘겨 받기 위해 써줍니다. "Parameter Specificition" 이라고 부르고요. 아무런 파라미터를 받지않는 경우에는 생략하거나 void 로 채워주면 됩니다.


3. 람다함수의 호출

위의 코드들은 람다 함수를 선언/정의(declaration/definition)만 해준 것이고, 실제로 코드 내에서 이 람다함수를 호출하기 위해서는 람다 함수의 바디 맨 끝 부분에 "function operator"인 ()를 붙여줘야 합니다. 

아래처럼요.

    1 #include <iostream>

    2 int main()

    3 {

    4     []{std::cout<<"Hello World"<<std::endl;}();

    5     return 0;

    6 }


4. 람다함수 파라미터

이번에는 람다함수에 파라미터를 넘겨봅시다. 예를 하나 들면, 아래와 같습니다.

[](int i){std::cout<<"Passed argument "<<i<<std::endl;};

간단하죠? 일반 함수를 사용하는 것과 같습니다. 단지 일반함수와 다른 점 하나는 default argument parameter를 가질 수 없다는 것입니다.

실제 코드에서 사용하려면, function operator인 ()를 이용해 파라미터를 넘겨주면 됩니다.

아래 처럼요.

    1 #include <iostream>

    2 #include <string>

    3 int main()

    4 {

    5     [](std::string name){std::cout<<"Hello "<<name<<std::endl;}("World"); 

    6     //print "Hello World"

    7     return 0;

    8 }


5. 람다함수 리턴값

이번에는 람다함수의 리턴값을 알아봅시다. 예제를 보죠.

bool isTrue = [](){return false;}();


isTrue라는 변수에 false 라는 값을 셋팅했습니다. 사실 이 코드는 아래 코드와 동일합니다.

bool isTrue = []()->bool{return false;}();

->bool 이 추가된 것 말고는 바뀐 점이 없습니다. 바뀐 코드에서는 명시적으로 리턴타입을 지정해준 것입니다. 

이런 식으로 람다함수에 명시적인 리턴 타입을 지정할 수 있습니다만, 만약 리턴타입이 생략되면 컴파일러는 람다함수 바디 내부에서 묵시적으로 리턴값을 추정하여 자동으로 셋팅해줍니다. 여기서 중요한 점은 컴파일러가 추정할 수 있어야 합니다! 컴파일러가 추정할 수 있으려면, 람다 함수 바디 내부에서 return statement 를 찾을 수 있어야 하고, 찾을 수 있으려면 return 구문이 바로 등장해야 합니다. 만약 찾을 수 없다면 디폴트 리턴타입인 void 로 셋팅합니다. 

아래 코드는 에러입니다.

    1 #include <iostream>

    2 int main()

    3 {

    4     bool isTrue = [](int n){

    5         if(n > 0)

    6             return true;

    7         else

    8             return false;

    9     }(30);

   10     return 0;

   11 }

   12 //error C2440: 'initializing' : cannot convert from 'void' to 'bool'

   13 //error C3499: a lambda that has been specified to have a void return type cannot return a value


컴파일러는 "return true;" 나 "return false;" 구문이 if 절 안에 내포되있기 때문에, 묵시적인 리턴 타입 추출에 실패합니다. 바로 찾지를 못하는 거죠.

또한 람다 함수 정의시 리턴 타입이 생략되있으므로, 컴파일러는 리턴타입을 일단 디폴트 타입인 void 로 추정합니다.  그러나 람다 함수 바디 안의 if 절 내부에서 return 을 하는 구문을 만났기 때문에 위와 같은 에러를 내는 겁니다. 따라서 이런 경우엔 명시적으로 리턴타입을 지정해줘야만 합니다.

위코드는 간단히 

[](int n)l{ 

를 

[](int n)->bool{

로 고치면 에러가 나지 않습니다.

다른 방법으로는 ternary operator를 사용해서 return 구문을 한 라인으로 바꿔주면, 컴파일러가 묵시적으로 리턴타입을 추론하게 할 수 있습니다.


    1 int main()

    2 {

    3     bool isTrue = [](int n){ return (n>0)? true:false;}(30);     

    4     return 0;

    5 }

  

6. 람다함수의 Capture Clause

다음으로 Lambda introducer 인 [] 에 대해 알아봅시다. [] 는 다른말로 Capture clause 라고 말합니다.

capture의 사전적 의미는 "Take into one's possession or control by force" 즉, 남의 것을 자기 것으로 만드는 겁니다. 그럼 뭘 캡쳐한다는 걸까요? 람다함수 밖에 선언된 (람다함수를 감싸고 있는 범위 내의) 변수를 캡쳐한다는 의미입니다. 

일례로 앞의 Lambda1 의 포스트에서 쓴 예제코드를 다시 보겠습니다.

    1 [&](int i)

    2 {

    3     if(i%2 == 0)

    4         evenSum += i;

    5     else

    6         oddSum += i;

    7 }

첫번째 라인에서 [&] 라고 썼는데, 이렇게 쓰면 람다 외부에서 선언된 모든 변수(evenSum 과 oddSum을 포함해서)를 람다함수 내부에서 레퍼런스 타입으로 갖다 쓰겠다는 겁니다. 그렇기 때문에 이 두 변수를 람다 함수 내부에서 따로 정의하지 않았지만 쓸 수 있었던 겁니다. 그리고 또한 레퍼런스로 캡쳐하기 때문에 람다 내부에서 연산한 내용이 그대로 원래 변수의 값에도 반영이 됩니다. 

값으로 캡쳐할 수도 있습니다. 외부 변수를 간단히 넘겨받아, 연산만 수행할 때는 값으로 캡쳐해도 됩니다. 그때는 & 를 붙이지 않고 써줍니다. 가령 evenSum, oddSum 을 값으로 넘겨받겠다면, [evenSum, oddSum] 이렇게 써주면 되고, 이때 람다 함수 내부에서 변경된 evenSum 과 oddSum 은 외부에 반영되지 않습니다.

외부의 선언된 변수 x, y를 레퍼런스로 캡쳐하겠다면 [&x, &y] 이렇게 써주면 되고, 이때는 내부의 변경이 그대로 외부로 반영됩니다.

그냥 [] 라고 쓰면 아무 것도 캡쳐하지 않겠다는 뜻이지만, 람다함수는 무조건 [] 로 시작해야 컴파일러가 이걸 람다함수라고 알 수 있기 때문에, 캡쳐할 것이 아무것도 없다 해도 무조건 써줘야 됩니다.

정리하면 이렇습니다.


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

[&x] : x 만 레퍼런스로 캡쳐하고, 다른 변수는 캡쳐하지 않음.

[x] : x 만 값으로 캡쳐하고, 다른 변수는 캡쳐하지 않음.

[&] : 모든 외부 변수를 레퍼런스로 캡쳐.

[=] : 모든 외부 변수를 값으로 캡쳐.

[x, y] : x와 y 를 값으로 캡쳐.

[&x, y] : x는 레퍼런스로 y는 값으로 캡쳐.

[x, &y] : x는 값으로 y는 레퍼런스로 캡쳐.

[&x, &y] : x 와 y 를 레퍼런스로 캡쳐.

[&, x] : x를 제외한 모든 변수를 레퍼런스로 캡쳐. (x는 값으로 캡쳐)

[=, &x] : x를 제외한 모든 변수를 값으로 캡쳐. (x는 레퍼런스로 캡쳐)


다음은 에러입니다.


[=, &] : 모든 값을 값 또는 레퍼런스로 캡쳐하려면 둘중의 하나를 선택해야 합니다.

[&x, =] : = 가 먼저 나와야 합니다.

[x, &] : & 가 먼저 나와야 합니다.

* 스태틱 변수는 캡쳐할 수 없습니다. automatic storage duration 을 갖는 일반 변수만 캡쳐할 수 있습니다.


7. 람다함수의 mutable 키워드

값으로 캡쳐된 외부 변수는 const의 특징을 갖기 때문에 람다 바디 내에서 수정할 수 없습니다. 그러나, mutable 이라는 키워드를 통해 값으로 캡쳐된 외부 변수라도 람다 바디내에서 수정이 가능합니다.

하지만, 이렇게 바디내에서 수정이 가능하더라도 값으로 캡쳐되었기때문에 여전히 외부 변수에 반영되지는 않습니다.


int x = 0;

[=]()mutable

{

     x++;

}();

mutable 키워드를 사용하려면 파라미터 스펙("()" 기호)을 반드시 명시적으로 지정해줘야 합니다.


8. 람다 함수 활용

람다 함수는 auto 키워드를 이용해 변수에 저장할 수 있고, 또한 람다 함수 내부에서 다른 람다 함수를 호출 할 수 있습니다. 


    1 #include <iostream>

    2 #include <string>

    3 #include <array> //array

    4 #include <algorithm> //sort

    5 #include <functional> //less

    6 

    7 int main()

    8 {       

    9 

   10     std::array<int, 10> arr = {3,2,1,4,9,10,7,8,4,6};

   11 

   12     //Assign lambda function to variable using "auto" keyword.

   13     auto ShowOriginalArray = [=]()

   14     {

   15         std::for_each(arr.cbegin(), arr.cend(), [](int n)

   16         {

   17             std::cout<<n<<",";

   18         });

   19         std::cout<<std::endl;

   20     };

   21     //call lambda function

   22     ShowOriginalArray();

   23 

   24     //sort array

   25     std::sort(arr.begin(), arr.end(), std::less<int>());

   26 

   27 

   28     //show array after sort

   29     auto ShowSortedArray = [=]()

   30     {

   31         std::for_each(arr.cbegin(), arr.cend(), [](int n)

   32         {

   33             std::cout<<n<<",";

   34         });

   35         std::cout<<std::endl;

   36     };

   37     ShowSortedArray();

   38 

   39     //show original array

   40     ShowOriginalArray();

   41 

   42     return 0;

   43 }

   44 

라인 13을 보면, 정의한 람다함수를 ShowOriginalArray 변수에 저장한 것을 알 수 있습니다. 그리고 라인 22에서 () 연산자를 이용해 이를 호출하고 있습니다.

라인 13 부터 라인 20 을 보면, 람다함수 내부에서 또 다른 람다 함수를 호출하는 것을 알 수 있습니다.

또한 람다함수에서 값으로 캡쳐하여 연산한 결과는 외부 변수의 변화와 관계없이 그 값이 유지됩니다.


아래 실행결과를 보면 배열인 arr 이 sort 되었지만, ShowOriginalArray() 호출은 여전히 sort 되기 전의 값을 보여주는 것을 알 수 있습니다.


왜 람다함수가 functor 를 계승했다고 하는지를 여실히 보여주는 예입니다. Functor 와 마찬가지로 Lambda도 State 를 가질 수 있다는 겁니다.

저작자 표시 비영리 변경 금지
Posted by In search of dream 꿈찾아고고씽
TAG C++, C++11

지난 포스트에서는 C++11에 새롭게 도입된 람다를 설명하기 위해 functor의 개념을 설명했습니다. Functor가 C++11에 새롭게 등장한 개념이 아닌데도 이를 길게 포스팅한 이유는 람다가 Functor와 그 성격이 동일하면서도, 이를 한층 업그레이드 시킨 개념이기 때문입니다.

물론 람다가 functor가 가지는 성격외의 수많은 장점들이 있습니다만, 이번 포스팅에서는 람다와 functor간의 비교에 집중해서 보도록 하겠습니다. 람다에 대한 기본 문법은 다음 포스트에서 다루고, 오늘은 일단 가볍게 어떻게 생겼는지 맛만 보도록 하겠습니다.

개인적으로 람다가 functor에 비해 나아진점 두 가지를 꼽아본다면,


1. 손꾸락이 편하다.

실제로 STL 알고리즘을 설명하는 C++ 책의 예제 코드를 보더라도, for_each 보다는 for 문을 쓰는 코드들이 더 많습니다. 왜 그럴까요. 저의 개인적인 추측이긴 하지만, 책의 저자마저도 for_each 에 넘겨줄 functor 를 정의해야 하는 걸 무지 귀찮아한건 아닐까 합니다. 사실 저도 stl 알고리즘을 사용할 때, 코드도 더 길어지고 읽기도 힘든 for_each 보다는 for문을 더 많이 썼었습니다. 

STL에서 제공하는 for_each 알고리즘이 성능면이나 컴파일러 최적화 면에서 우수하다고 아무리 떠들어 대더라도, 코딩에 지친 프로그래머들에게는 당장 손꾸락이 편한 for 문이 훨씬 달콤했던 겁니다.

vector를 순회하던 예전 방식입니다.

    1 #include <vector>

    2 #include <iostream>

    3 

    4 int main(int argc, char** argv)

    5 {

    6     std::vector<int> v;

    7     v.push_back(1);

    8     v.push_back(2);

    9     //...

   10     for(auto it = v.begin(); it != v.end(); it++)

   11     {

   12         std::cout<<*it<<std::endl;

   13     }

   14     return 0;

   15 }

   16 


이제는 람다를 이용해 봅시다.

    1 #include <vector>

    2 #include <iostream>

    3 #include <algorithm>

    4 

    5 int main(int argc, char** argv)

    6 {

    7     std::vector<int> v;

    8     v.push_back(1);

    9     v.push_back(2);

   10     v.push_back(3);

   11     v.push_back(4);

   12     v.push_back(5);

   13 

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

   15 

   16     return 0;

   17 }


2. Readability 증가

STL 에서 functor를 쓰려면 항상 먼저 functor 용 클래스를 선언해줘야 했습니다. 이렇게 선언한 functor용 클래스는 호출되는 코드 바로 위에 선언될 수도 있겠지만, 그렇지 않을 수도 있습니다. cpp 파일 맨 꼭대기에 선언되 있을 수도 있고, 다른 헤더 파일 내에 정의되있을 수도 있습니다. 이에 대한 결정은 전적으로 프로그래머에게 달려 있기 때문입니다.

이에 반해 람다는 사용하는 바로 그 지점에서 바로 정의해서 사용합니다. 이를 통해 더 이상 functor의 원형을 찾아 헤매지 않아도 됩니다. 다른 이야기지만, C에서 C++로 넘어오면서, 가장 혁신적인 변화는 클래스의 도입으로 인한 OOP 컨셉이 가능해진 것이라고 알려져 있습니다. 그러나, 제 생각은 클래스 도입에 버금가는 변화가 변수선언을 함수 맨 상단에 하지 않아도 되는 것이라고 생각합니다. C++ 에서는 어느 위치에서든 사용하기 바로 전에 선언/정의를 하기만 하면, 사용할 수 있는 것이죠. 이런 변화를 통해 C++는 C에 비해 획기적으로 Readibility를 증가시켰습니다. 람다도 마찬가지 맥락에서 바라볼 수 있다고 생각합니다. 다른 위치에 functor 를 미리 정의해 사용하는 대신, 사용하는 바로 그 장소에서 (in-place) 편하게 함수를 정의해서 쓰는 겁니다.

예전 functor를 다뤘던 포스트에 나온 소스코드를 lambda를 이용해 수정해봤습니다.

아래는 Functor를 사용했던 예제입니다.

    1 #include <iostream>

    2 #include <algorithm>

    3 #include <vector>

    4 

    5 class CIsOdd

    6 {

    7 public:

    8     bool operator()(int i)

    9     {

   10         return ((i%2)==1);

   11     }

   12 };

   13 

   14 int main (int argc, char** argv)

   15 { 

   16     std::vector<int> v; 

   17 

   18     v.push_back(10);

   19     v.push_back(25);

   20     v.push_back(40);

   21     v.push_back(55);

   22     CIsOdd objIsOdd;

   23     //Using Functor

   24     auto it = std::find_if(v.cbegin(), v.cend(), objIsOdd);

   25 

   26     std::cout << "[Using functor]: The first odd value is " << *it << std::endl; //25

   27     

   28     return 0;

   29 }


아래는 위를 람다로 바꿔 쓴 코드입니다.

    1 #include <iostream>

    2 #include <algorithm>

    3 #include <vector>

    4 

    5 int main (int argc, char** argv)

    6 { 

    7     std::vector<int> v; 

    8 

    9     v.push_back(10);

   10     v.push_back(25);

   11     v.push_back(40);

   12     v.push_back(55);

   13     //Using Lambda

   14     auto it2 = std::find_if(v.begin(), v.end(),

   15         [](int i)->bool

   16         {

   17             return (i%2) == 1;

   18         }

   19     );

   20 

   21     std::cout << "[Using lambda]: The first odd value is " << *it2 << std::endl; //25

   22     return 0;

   23 }


다른 예제도 람다로 바꿔봤습니다.

Functor를 썼던 아래 예제코드

    1 #include <iostream>

    2 #include <algorithm>

    3 #include <array>

    4 

    5 class EvenOddFunctor

    6 {

    7 public:

    8     EvenOddFunctor(): evenSum(0), oddSum(0){}

    9 

   10     void operator()(int x)

   11     {

   12         if(x%2 == 0)

   13             evenSum += x;

   14         else

   15             oddSum += x;

   16     }

   17     int sumEven() const { return evenSum;}

   18     int sumOdd() const { return oddSum;}

   19 

   20 private:

   21     int evenSum;

   22     int oddSum;

   23 

   24 };

   25 

   26 int main(int argc, char** argv)

   27 {

   28     EvenOddFunctor functor;

   29     std::array <int, 10> theList = {1,2,3,4,5,6,7,8,9,10};

   30     functor = std::for_each(theList.cbegin(), theList.cend(), functor);

   31 

   32     std::cout<<"Sum of evens: "<<functor.sumEven()<<std::endl;

   33     std::cout<<"Sum of odds: "<<functor.sumOdd()<<std::endl;

   34 

   35     getchar();

   36     return 0;

   37 }


아래와 같이, 람다를 이용하면 좀 더 간결하고 직관적으로 바뀔 수 있습니다.

    1 #include <iostream>

    2 #include <algorithm>

    3 #include <array>

    4 

    5 int main(int argc, char** argv)

    6 {

    7     std::array <int, 10> theList = {1,2,3,4,5,6,7,8,9,10};

    8 

    9     int evenSum = 0, oddSum = 0;

   10     std::for_each(theList.begin(), theList.end(), [&](int i){

   11         if(i%2 == 0)

   12             evenSum+=i;

   13         else

   14             oddSum+=i;

   15     });

   16     std::cout<<"[Using lambda]: Sum of evens: "<<evenSum<<std::endl;

   17     std::cout<<"[Using lambda]: Sum of odds: "<<oddSum<<std::endl;

   18     getchar();

   19     return 0;

   20 }

저작자 표시 비영리 변경 금지
Posted by In search of dream 꿈찾아고고씽
TAG C++, C++11

C++11 에는 Lambda 라고 하는 기능이 추가됬습니다.

일단 C++11 에 추가된 Lambda를 이해하기 위해서는 왜 이런 개념이 나왔는지를 먼저 이해하는게 중요할거 같습니다. 사람들은 언제나 조금이라도 불편한 것을 해결하려는 방향으로 일을 진행하니까 말이죠.

Lambda의 탄생 배경에는 함수 객체라고 불리는 function object (또는 functor) 가 엮여 있습니다. 좀더 정확하게 말하자면, 함수 객체를 사용하는데 불편을 느낀 사람들이 있었고 이에 Lambda가 탄생한 것입니다라고 말하고 싶지만, 사실, 다른 언어에서는 이미 Lambda의 개념이 구현되있었기 때문에 C++에서도 질 수없다라는 심정으로 만든건 아닌가 싶기도 합니다. ㅎㅎ

함수 객체는 그리 복잡한 개념은 아니지만, 모르는 분들을 위해 이 포스트에서는 Lambda가 아닌 함수객체에 대해 설명하는 것으로 하고, Lambda는 다음 포스트에 올리겠습니다. 다시 한번 말하지만, Functor 는 C++11에 새롭게 추가된 기능이 아닙니다. 


자. 그럼. 함수객체 (Functor, Function Object) 란 뭘까요.

함수 객체란 객체를 마치 함수처럼 사용하기 때문에 붙여진 이름입니다. 객체를 함수처럼 쓴다는게 무슨 말일까요? 함수는 보통 뒤에 ()가 붙잖아요? 하지만, 클래스의 객체에는 뒤에 ()를 붙이지 않습니다. 클래스의 객체를 함수처럼 쓴다는건, 객체 뒤에 () 를 붙여서 사용한다는 겁니다. 

그러나 그냥 마구잡이로 붙여 준다고 해서 컴파일러가 알아 먹을리가 없습니다. 그래서 컴파일러가 알아먹게 해주기 위해, 함수객체용 클래스 내에서 function call operator 라고 부르는 () 연산자를 오버로딩 해줍니다.

아래 코드처럼요

    1 #include <iostream>

    2 

    3 class Functor

    4 {

    5 public:

    6     void operator ()()

    7     {

    8         std::cout<<"Simple Functor"<<std::endl;

    9     }

   10 };

   11 

   12 int main(int argc, char** argv)

   13 {

   14     Functor func;

   15     func();

   16     return 0;

   17 }


위 예제에서 라인 3 ~ 10 까지, Functor 라는 이름의 함수 객체용 클래스를 정의했습니다. 멤버변수라고는 달랑 하나입니다. 그것도 () 연산자 (function call operator 라고 합니다) 를 오버로딩한 것 뿐 입니다.

제가 위에서 정의한 Functor 라는 이름의 클래스가 세상에서 가장 간단한 함수객체 클래스의 한 예입니다. 클래스 내부에서 function call operator만 오버로딩 해주면 클래스의 함수객체로서의 조건은 만족됩니다. 이제부터 다른 사람이 작성한 클래스를 봐야할 때, 저렇게 function call operator 를 오버로딩한 부분이 있으면 일단, "아, functor 용 클래스구나.." 이렇게 생각하시면 됩니다.

Functor를 가져다 사용하는 코드인 main 함수 부분을 보죠. 14 라인에서 func 라는 이름으로 객체를 하나 만들었습니다. 그리고, 라인 15에서 그 객체 뒷 부분에 function call operator를 떡하니 붙여놨습니다. func 변수는 분명히 Functor 클래스의 객체지만 뒤에 () 연산자를 붙여주니까, 마치 함수처럼 보이지 않습니까? 그래서 이걸 function object 또는 functor 라고 부르는 겁니다. 좀 허탈하신가요? ㅎㅎ

functor 에 대한 영어식 정의는 이렇습니다.

A functor is a object which acts liks a function, and this object basically is a instance of class which defines function call operator as a member.

Functor 어렵지 않죠?


함수객체의 문제점

하지만, 예제 코드처럼 간단한 코드에서는 금방 알아보겠는데, 만약 코드가 길어진다면 좀 헷갈릴 수도 있겠네요. 함수 객체용 클래스가 호출되는 코드 바로 위에 찰싹 들러붙어 있으리란 법도 없고, 클래스 설계자가 클래스 재사용을 위해 어떤 어떤 헤더에 정의해두고, 잘 안보이는 곳에 짱박아 놨을 수도 있고요. 예제코드의 라인 15만 보면, 이 코드가 함수 객체를 쓴건지 아니면 그냥 일반 함수 호출인지 아리까리 하기도 합니다.



그럼에도 불구하고, 이런 기법이 왜 필요한걸까요. Functor에는 포기할 수 없는 아주 중요한 이점이 있기 때문입니다.

 

Functor의 첫번째 중요한 특은 일반 함수와는 다르게 상태정보(state)를 저장할 수 있다는 겁니다.

아래 코드를 보세요.

    1 #include <iostream>

    2 

    3 struct Accumulator

    4 {

    5     Accumulator()

    6     {

    7         counter = 0;

    8     }

    9     int counter;

   10     int operator()(int i)

   11     {

   12         return counter += i;

   13     }

   14 };

   15 

   16 int main(int argc, char** argv)

   17 {

   18     Accumulator a;

   19     std::cout<<a(10)<<std::endl; //print "10"

   20     std::cout<<a(20)<<std::endl;// print "30"

   21 

   22     return 0;

   23 }


Accumulator라는 functor 클래스의 인스턴스 a를 만들어주고, 마치 함수처럼 쓰고 있습니다. 10을 넣어주고 출력하고, 20을 넣어주고 출력합니다. 새로운 값들을 넣어줄 때마다 예전에 넣어줬던 값들이 계속 누적되어 합산됩니다. 이런 상태(state) 는 a 라는 객체가 소멸될때까지 지속됩니다.

이에 반해 일반 함수는 어떤가요. 뭔가 파라미터를 받아서, 연산을 한 후 리턴하고나서 끝나 버립니다. 예전에 실행했던 연산의 결과를 기억하지 않습니다. 일반함수의 이런 특징과 비교해 볼 때 Functor는 자기가 만들어진 상태(state)를 소멸되기 전까지 계속 기억하고 있습니다. 바로 이런 특성때문에 Functor는 state를 저장할 수 있다고 말합니다.

Functor의 두 번째 중요한 특성은 STL 의 수많은 알고리즘들이 Functor를 사용한다는 겁니다. 이걸 과연 특성이라고 부르는게 맞는건지 모르겠군요. 하지만 분명히 STL 알고리즘은 Functor를 많이 씁니다.

이를 설명하기 전에 STL Algorithm 에 대해 잠깐 설명하고 넘어가죠. Functor를 모르는 분들은 STL Algorithm 도 모를 가능성이 높기때문에 ㅋㅋ

가령, std::algorithm 가운데 std::find_if 라는게 있습니다. API 다큐먼트를 보면 아래처럼 되있습니다.

(http://www.cplusplus.com/reference/algorithm/find_if/)

template <class InputIterator, class Predicate>   
InputIterator find_if ( InputIterator first, InputIterator last, Predicate pred );

Find element in range

Returns an iterator to the first element in the range [first,last) for which applying pred to it, is true.

The behavior of this function template is equivalent to:
1
2
3
4
5
6
template<class InputIterator, class Predicate>  
InputIterator find_if ( InputIterator first, InputIterator last, Predicate pred )  
{    
  for ( ; first!=last ; first++ ) if ( pred(*first) ) break;    
  return first;  
}



Parameters
first, last
Input iterators to the initial and final positions in a sequence. The range used is [first,last), which contains all the elements between first and last, including the element pointed by first but not the element pointed by last.
pred
Unary predicate taking an element in the range as argument, and returning a value indicating the falsehood (withfalse, or a zero value) or truth (true, or non-zero) of some condition applied to it. This can either be a pointer to a function or an object whose class overloads operator().

Return value

An iterator to the first element in the range for which the application of pred to it does not return false (zero).
If pred is false for all elements, the function returns last.


find_if라는 함수가 하는 일은 어떤 주어진 범위를 검색해서, 조건에 부합하는 엘리먼트를 검색한 후, 그 첫번째 엘리먼트를 리턴하는 겁니다. 파라미터를 설명한 부분을 보면 첫번째, 두번째에는 입력용 iterator를 주고, 마지막에 Predicate 라는 타입의 변수를 주도록 되어 있습니다.

마지막 파라미터인 Predicate 란 뭘까요. 이를 설명하기 전에 먼저 알고 있어야 할 것이 있습니다. STL에서 사용하는 function object는 세 가지 종류로 나눠집니다. Generator, Unary Function 그리고 Binary Function 입니다. "func()" 처럼 인자없이 호출되는 것을 Generator라고 하고, "func(x)" 처럼 하나의 인자를 받는 것을 Unary Function, "func(x, y)" 처럼 두개의 인자를 받는 것을 Binary Function 이라고 합니다. 그런데, STL에서는 Function object 중에 bool 값을 리턴하는 것들에게는 조금 특별한 이름을 지어줬습니다. bool 을 리턴하는 Unary function 은 Predicate 또는 Unary Predicate 라고 부르고, bool 을 리턴하는 Binary function 은 Binary Predicate 라고 부릅니다. 만약 세 개를 받으면요? ternary predicate 라고 부를 수 있겠지만, STL Algorithm 함수에 두 개 이상의 파라미터를 받는 경우는 없기 때문에 그런 말은 사용하지 않습니다.

다시 find_if의 API  문서로 돌아가서, Predicate 부분의 마지막 단락을 읽어보면, "Predicate로는 함수에 대한 포인터 또는 operator() 를 오버라이드한 클래스의 객체가 될 수 있다" 라고 되어 있습니다. 
아 그렇군요. 이 operator()를 오버라이드한 클래스의 객체란 결국 functor를 설명한 말이네요. STL 알고리즘 함수에서 매개변수로 Functor 를 사용하고 있는 것입니다. 이 외에도 많은 STL 알고리즘 함수들이 매개변수로 Functor를 사용하고 있습니다.

다른 함수도 한번 보죠. for_each 라는 알고리즘 함수가 있는데, 이 함수는 주어진 리스트의 각 엘리먼트를 돌면서 각각에 특정 Functor 또는 함수 포인터를 적용하는 함수입니다. 아래 코드는 for_each를 이용하여, 주어진 정수 리스트에서 짝수는 짝수끼리, 홀수는 홀수끼리 값을 더한 후 이를 출력하는 코드입니다.


    1 #include <iostream>

    2 #include <algorithm>

    3 #include <array>

    4 

    5 class EvenOddFunctor

    6 {

    7 public:

    8     EvenOddFunctor(): evenSum(0), oddSum(0){}

    9 

   10     void operator()(int x)

   11     {

   12         if(x%2 == 0)

   13             evenSum += x;

   14         else

   15             oddSum += x;

   16     }

   17     int sumEven() const { return evenSum;}

   18     int sumOdd() const { return oddSum;}

   19 

   20 private:

   21     int evenSum;

   22     int oddSum;

   23 

   24 };

   25 

   26 int main(int argc, char** argv)

   27 {

   28     EvenOddFunctor functor;

   29     std::array <int, 10> theList = {1,2,3,4,5,6,7,8,9,10};

   30     functor = std::for_each(theList.cbegin(), theList.cend(), functor);

   31 

   32     std::cout<<"Sum of evens: "<<functor.sumEven()<<std::endl;

   33     std::cout<<"Sum of odds: "<<functor.sumOdd()<<std::endl;

   34 

   35     getchar();

   36     return 0;

   37 }



STL은 Functor를 사랑합니다. 

위 두 예를 통해 STL 알고리즘에서 얼마나 Functor를 많이 쓰는지 이해하셨으리라 생각합니다. 사용자가 STL 알고리즘을 사용해야 할때 마다 functor를 작성해야 하는 수고를 덜어주기 위해, STL 내부에는 쓸만한 Functor 클래스들이  미리 정의되있습니다.  Functor를 무작정 만들어서 사용하기 전에 미리 STL에 미리 구현된 Functor들을 한번 살펴보고, 적절한게 없을때 만들어서 쓰는 편이 좋겠습니다. 이 Functor 들은 functional 이라는 헤더 라이브러리 안에 정의되있습니다.



STL에 미리 정의된 Functor의 예를 하나 보죠. 어떤 범위의 값을 정렬하기 위해 sort 알고리즘에서 사용될 용도로 less 라는 functor class 가 있습니다. 원형을 보면 아래처럼 생겼습니다.

template <class T> 
struct less: binary_function<T, T, bool>
{
     bool operator() (const T& x, const T& y) const
     {
          return x<y;
     }
}

이해하기 어렵지 않죠? 전형적인 functor의 모습을 띄고 있네요. binary_function 이라는 클래스를 상속받아 구현되어 있고, 내부에서는 역시 () 오퍼레이터를 오버로딩하고 있습니다. () 오퍼레이터의 구현부분을 보면, x 와 y라는 두 값을 입력으로 받아, x가 y보다 작으면 true를 리턴합니다. 

less functor 의 대표적인 사용 예는 리스트 내부의 값을 오름 차순으로 정렬할 때 입니다.


    1 #include <functional>//less

    2 #include <iostream>

    3 #include <algorithm>//sort

    4 #include <array> //array

    5 

    6 int main(int argc, char** argv)

    7 {

    8     std::array<int, 5> intArr = {30, 20, 1, 55, 1000};

    9     std:sort(intArr.begin(), intArr.end(), std::less<int>());

   10     auto it= intArr.cbegin();

   11     while(it != intArr.cend())

   12         std::cout<<*it++<<std::endl;

   13 

   14     return 0;

   15 }



위 코드를 실행하면 intArr 라는 배열안의 엘리먼트들은 낮은 순서인 1, 20, 30, 55, 1000 의 값으로 정렬되고, 이 값을 순서대로 출력하고 종료합니다. 라인 9를 보면 std::less<int>() 라는 functor 를 그대로 써줬습니다. less functor는 클래스 템플릿으로 구현되있기 때문에 리스트안의 값이 정수가 아닌 다른 타입일때는 해당 타입을 <> 안에 써주면 됩니다.


마지막으로, 함수포인터와 Functor를 비교해보죠.

아래 코드에 STL 알고리즘 함수에 함수 포인터를 사용한 경우와 Functor 를 사용한 경우 두 가지 경우를 사용한 예를 나타내 봤습니다.

    1 #include <iostream>

    2 #include <algorithm>

    3 #include <vector>

    4 

    5 bool IsOdd (int i)

    6 {

    7     return ((i%2)==1);

    8 }

    9 

   10 class CIsOdd

   11 {

   12 public:

   13     bool operator()(int i)

   14     {

   15         return ((i%2)==1);

   16     }

   17 };

   18 

   19 //#define _USE_FUNC_POINTER_

   20 

   21 int main (int argc, char** argv)

   22 { 

   23     std::vector<int> v; 

   24 

   25     v.push_back(10);

   26     v.push_back(25);

   27     v.push_back(40);

   28     v.push_back(55);

   29 #ifdef _USE_FUNC_POINTER_

   30     auto it = find_if (v.begin(), v.end(), &IsOdd);

   31 #else

   32     CIsOdd objIsOdd;

   33     auto it = find_if(v.cbegin(), v.cend(), objIsOdd);

   34 #endif//

   35     std::cout << "The first odd value is " << *it << std::endl;

   36     return 0;

   37 }


위코드는 일단 Functor를 사용하도록 되어있고요, 함수포인터를 쓴 결과를 보려면 중간의 #define 으로 된 부분을 주석해제하면 됩니다. 

함수 포인터에서는, IsOdd 함수를 포인터 형태로 세번째 파라미터로 제공해 줬습니다. IsOdd() 라는 함수의 이름 자체가 함수가 위치한 메모리를 가르키기 때문에 IsOdd 라고만 써도 되고, &IsOdd의 형태로 넘겨줘도 상관 없습니다. Functor 에서는 일단 functor 를 하나 생성해준 후 이를 넘겨주고 있습니다.

양쪽 모두 실행 결과는 같습니다. 어느쪽을 사용해도 됩니다. 편한대로 쓰면 되는거죠.


함수 포인터 vs Functor

그러나 함수 포인터와 Functor 모두 장단점이 있는데, 알고 지나가는게 좋겠습니다. 
Function Pointer 는 간결하게 쓱싹 작성할 수 있는데 반해, Functor 는 코딩하기 좀 귀찮은 면이 있습니다. Functor를 만들려면, functor용 클래스 및 () 오퍼레이터를 오버로딩 해줘야 됩니다. Function Pointer 는 간단하게 쓱삭 작성할 수 있는데 비해, 클래스의 멤버가 아닌 전역 함수로 작성될 가능성이 높죠. 간단하게 금방 쓰고 말건데, 클래스 멤버로 들어가는 것도 아닌거 같고 말입니다. 사실 이건 Functor 도 마찬가지죠. namespace 를 어지럽히는 주범이 될 확률이 높습니다.

하지만, Functor는 앞서 말한대로 state 정보를 가질 수 있기 때문에, 함수포인터용 일반 함수에 비해 Customizing이 편리합니다.

다른 한편, 실행 측면에서 보면, 함수 포인터로는 컴파일러가 어떤 함수를 사용해야 할지 Compile 타임에 알수 없고 런타임이 되고 나서야 실제 사용할 함수를 알아차릴 수 있습니다. 이에 반해 Functor 는 컴파일러가 컴파일타임에 알 수 있기 때문에, functor를 사용하면 컴파일러에 의해 inline 의 대상으로 선정될 수 있습니다. 컴파일러가 잘만 봐주시면, 성능상의 이점을 볼 수도 있습니다.



C++11 Lambda의 도입배경을 설명하기 위해 functor의 개념을 다시 한번 짚어봤습니다. 이걸 잘 이해하셨으면, 람다가 얼마나 편리한지 더 쉽게 이해하실 수 있지 않나 싶네요. 다음 포스트엔 진짜 Lambda 이야기를 해보도록 하겠습니다. ^^;;

저작자 표시 비영리 변경 금지
Posted by In search of dream 꿈찾아고고씽
TAG C++, C++11