C++ 이야기 열네번째: 함수 객체 #2

Posted at 2007. 6. 10. 23:54 // in S/W개발/C++ 이야기 // by 김윤수


C++ 이야기 열네번째입니다. 함수 객체 #1에 이어 이번 글에서도 함수 객체에 대해 다루어 보도록 하겠습니다.

지난 글에서 find_if() 알고리즘 사용예를 보여드렸던 것 기억하시나요 ? 사실 제가 지금까지 C++ 이야기를 써오면서 C++ 표준 라이브러리의 알고리즘에 대해서는 자세하게 설명한 적이 없는데, 불쑥 find_if() 사용예를 보여준다는 게 조금 고민되긴 했지만, 그냥 이번 글을 풀어가기 쉽게 하려고 사용예를 보여드렸습니다. 그런데 마침 Alones님이 std::find_if 사용해보기 라는 좋은 글을 알려 주셨습니다. find_if(beg, end, condition) 알고리즘은 [beg, end) 범위내에서 condition에 맞는 첫번째 원소를 찾아주는 일을 해 줍니다.

사실 C++ 표준 라이브러리에는 find_if() 알고리즘과 비슷하게 _if 접미사가 붙은 알고리즘들이 많습니다. 예를 들면, count_if(), replace_if(), replace_copy_if(), remove_if(), remove_copy_if() 등이 있습니다. 이런 알고리즘들은 사용자가 명시한 조건(이것때문에 이름에 _if() 접미사가 붙은 것입니다)이 만족되는 경우에만 앞에 기술된 알고리즘 count, replace 등을 실행합니다. 이렇게 하기 위해서는 당연히 사용자가 조건을 명시할 수 있도록 해줘야 하는데, 이걸 이름하여 조건자(영어로는 Predicate, 곽용재씨는 술어 구문이라고 번역하더군요. 저는 조건자가 더 맘에 드네요. 물론 조건자라고 하면 함수 보다는 함수 객체를 지칭하는 것 같아서 그렇긴 하지만...)라는 걸 통해서 전달해 줄 수 있도록 하고 있습니다.

그럼, 조건자라는 게 무엇이냐~하면 간단히 말해서 참과 거짓으로 판별할 수 있는 값을 리턴해 주는 함수 객체(또는 함수)를 말합니다. 실상 C/C++ 에서는 임의의 정수형 값들은 boolean 값으로 변환될 수 있기 때문에 bool 값을 리턴하는 것뿐 아니라 정수형 값을 리턴하는 함수 객체(또는 함수)도 잠재적으로 조건자가 될 수 있습니다. 지난 번 find_if() 예제에서 쓰인 modulus<int>() 도 조건자라고 할 수 있습니다.

pos = find_if(v.begin(), v.end(), not1(bind2nd(modulus<int>(), 2)));

근데, 함수 객체로된 조건자를 작성하실 때는 주의하셔야할 점이 있습니다. 호출될 때 함수 객체의 상태가 바뀌면 안된다는 점입니다. 예를 들어, 다음과 같은 코드를 수행하면 어떤 일이 벌어질까요 ? 일부러 호출할 때마다 상태를 변경하는 함수 객체-Nth-를 C++ Standard Library 인용해서 약간 수정해 봤습니다.

#include <iostream>
#include <list>
#include <algorithm>

using namespace std;

template <typename T>
struct Print {
  void operator() (T v) {
    cout << v << " ";
  }
};

// 호출될 때마다 상태를 변경하는 함수 객체
class Nth {
private:
  int _nth;
  int _count;

public:
  Nth(int nth): _nth(nth), _count(0) {}

  bool operator() (int) {
    return (++_count == _nth);
  }
};

// 아래 generate_n() 알고리즘과 함께 쓰이면서 start로부터 시작해서
// 연속된 정수형 숫자를 생성해 주는 함수 객체
class SequenceGenerator {
private:
  int _start;

public:
  SequenceGenerator(int start): _start(start) {}

  int operator() (void) {
    return _start++;
  }
};

int
main(void)
{
  list<int> l;

  // 1 ~ 10까지를 생성해 줍니다.
  // generate_n() 에 대해서는 일단 그냥 넘어가도록 하겠습니다
  generate_n(back_inserter(l), 10, SequenceGenerator(1));
  for_each(l.begin(), l.end(), Print<int>());
  cout << endl;

  // remove_if()는 주어진 조건자에 맞는 모든 요소를 없애줍니다.
  // 이 코드에서는 세번째 요소, 즉, 3만 없애줍니다.
  list<int>::iterator newend;
  newend = remove_if(l.begin(), l.end(), Nth(3));
  for_each(l.begin(), newend, Print<int>());
  cout << endl;
}


제일 마지막 결과가 여러분의 예상대로 "1 2 4 5 6 7 8 9 10"으로 나오나요 ? 돌려보진 않았지만 안 나올 것 같다구요 ? 예상대로 나온다면 제가 물어보지도 않았을테니 그렇게 안 나올 것 같다구요 ? 예~ 똑똑하시네요. 실제 제가 사용하는 컴파일러에서 돌려 보았더니 다음과 같이 결과가 나오네요.

1 2 3 4 5 6 7 8 9 10
1 2 4 5 7 8 9 10              // 어! 이상하게 6도 지워졌네요 ? 신기해라~!


이게 어떻게 된 조화일까요 ? 왜 '6'도 지워져 버리는 걸까요 ? 이유는 remove_if() 의 알고리즘 구현방식 때문에 그렇습니다. 다음은 제가 쓰는 컴파일러에서 remove_if()가 구현되어 있는 방식입니다. 아래 알고리즘에서 _FwdIt 는 forward iterator 를 _Pr 은 predicate(조건자)를 뜻하는 것으로 이해하시면 됩니다.

01: template<class _FwdIt, class _Pr>
02: inline _FwdIt remove_if(_FwdIt _First, _FwdIt _Last, _Pr _Pred)
03: { // remove each satisfying _Pred
04:   // 첫번째로 조건을 만족하는 요소의 위치를 찾습니다
05:   _First = std::find_if(_First, _Last, _Pred);
06:   if (_First == _Last)
07:     return (_First); // empty sequence, all done
08:   else
09:   { // nonempty sequence, worth doing
10:     _FwdIt _First1 = _First;
11:     // 조건을 만족하는 요소를 skip 하고(remove 효과) 그 다음 요소부터
12:     // _Pred 으로 검사해 가면서 _First 쪽으로 복사해 갑니다(덮어쓰기 효과)
13:     return remove_copy_if(++_First1, _Last, _First, _Pred));
14:   }
15: }

왜 위와 같은 결과가 벌어지는지 이해를 돕기 위해 그림으로 설명드리도록 하겠습니다. 아래 그림에서 제일 위쪽 부분의 그림은 위 알고리즘에서 05 라인을 수행한 후의 상태입니다. 그리고, 점선 바로 위쪽 부분은 13 라인을 호출할 때의 상태입니다. 그리고, 05 라인을 호출한 뒤에도 _Pred 의 상태는 값을 통한 넘김(call-by-value)의 특성에 따라 원래의 상태대로 그대로 남아있게 됩니다. 즉, _count 값이 0으로 계속 남아있게 됩니다. _Pred 은 또다시 원래 상태대로 remove_copy_if() 알고리즘에서 복사되어 넘어가게 되고, 이런 상태에서 remove_copy_if() 를 수행하면 점선 아래 쪽 부분과 같은 결과가 나타나게 됩니다. 즉, 의도하지 않았던 6에서도 매치가 발생하게 되는 거죠. 왜나하면 remove_copy_if() 입장에서는 4부터 시작했기 때문에 6이 세번째 요소가 되기 때문입니다.



사용자 삽입 이미지

크게하려면 클릭 하세요


이런 이유 때문에 호출되는 동안 자신의 상태를 변경하는 조건자를 사용하면 알고리즘이 제대로 동작하지 않을 수도 있습니다. 그러니, 조건자를 작성할 때는 호출되는 동안 상태를 변경할 생각일랑은 하지 않는 게 좋습니다. 나중에 괜히 어만 문제에 시달리기 싫다면 말입니다.

조건자는 아까 말씀드린 대로 꼭 함수 객체만 조건자가 될 수 있는 게 아니라 boolean 호환되는 타입을 리턴하는 함수라도 조건자가 될 수 있습니다. 예를 들어 여러분이 vector<char*> 를 통해 고객 이름 데이터 베이스를 관리하고 있는데, 특정 이름을 갖는 고객을 찾는 기능이 필요하다고 치겠습니다. 그렇다면 이런 기능을 구현하기 위해 가장 기본으로 쓸 수 있는 알고리즘이 find_if() 일 것입니다. 음... 다음과 같이 find_if()를 이용할 수 있지 않을까요 ?

vector<char*> vname;
......
vector<char*>::iterator pos;
pos = find_if(v.begin(), v.end(), (strcmp(*i, searchname) == 0));

위와 같이 작성할 수 있다면 정말 좋을 것입니다. 그렇지만 안타깝게도 위와 같은 코드는 전혀 문법에 맞질 않습니다. 우선 *i 는 컴파일러가 전혀 알 수 없는 변수이구요, 'strcmp(*i, searchname) == 0' 수식은 함수나 함수 객체가 아니기 때문에 '()' 연산자가 적용될 수가 없습니다. 그리고, 마지막으로 find_if() 는 인자가 하나인 조건자를 필요로 합니다. strcmp()는 두 개의 인자를 갖기 때문에 find_if()에 그대로 쓸 수가 없습니다.

이 말을 듣고 나면 앞이 캄캄한 분들이 있으실 겁니다. 아니 그렇다면, C 의 str 로 시작하는 왼갖 string 관련 함수를 하나도 못 쓴단 말이야 ? 에이 그럼 C++ 표준 라이브러리도 제대로 써 먹기 힘들겠네~ 오오오 이런 이런 그런 성급한 판단은 금물입니다. C++ 표준 라이브러리를 표준화한분들도 나름 똑똑한 분들이기 때문에 C 의 라이브러리 함수를 쓸 수 있는 방법을 마련해 놓았습니다. 일반 함수를 함수 객체처럼 바꿔주고, 이항 함수 객체를 단항 함수 객체로 바꾸어 주는 특수한 객체들을 마련해 놓았습니다. 이름하여 함수 어댑터!

우선 일반 함수를 함수 객체처럼 바꿔 주는 함수 어댑터 ptr_fun()입니다. 사용하실 때는 그냥 함수 이름을 ptr_fun() 으로 둘러싸 주기만 하면 됩니다. 다음과 같이 말입니다.

pos = find_if(v.begin(), v.end(), ptr_fun(strcmp));

근데, ptr_fun(strcmp) 가 여전히 이항 함수 객체라는 것이 문제네요. 이항 함수 객체를 단항 함수 객체로 바꾸어 주는 것은 지난 글 C++ 이야기 열두번째: 함수 객체 #1의 표에서 나열했던 것 중에 bind1st(), bind2nd() 입니다. bind1st()와 bind2nd()가 해주는 일은 각각 첫번째 인자 또는 두번째 인자를 특정 값으로 고정시켜주는 일을 합니다. 그럼, 다시 한 번 코드를 작성한다면 다음과 같이 수정해 볼 수 있을 것입니다.

pos = find_if(v.begin(), v.end(), bind2nd(ptr_fun(strcmp), searchname));

여기까지하면 일단 컴파일은 됩니다. 근데 결과가 맞지 않을 것입니다. 왜냐면 strcmp() 함수가 이름이 같으면 0을 리턴하고, 이름이 다르면 0이 아닌 값을 리턴하기 때문에 위 코드는 이름이 searchname이 아닌 첫번째 사람을 찾게 됩니다. 그렇다면 strcmp() 함수의 결과를 거꾸로 뒤집어야 원하는 결과를 얻을 수 있겠네요. 이럴 때 쓰는 함수 어댑터로 not1() 이라는 것이 있습니다. 그럼, 마지막으로 코드의 완성버전은 다음과 같습니다.

pos = find_if(v.begin(), v.end(), not1(bind2nd(ptr_fun(strcmp), searchname)));

위 코드를 작성해 놓고 보니 상당히 이해하기 어렵게 돼 버렸네요. 그래도 어찌 됐든 소기의 목적은 달성했습니다.

다음에는 멤버 함수를 함수 객체처럼 호출할 수 있게 해주는 mem_fun(), mem_fun_ref() 에 대해 설명드리도록 하겠습니다. (이번 글에서도 함수 객체를 다 끝내진 못했네요. 쩝~ TR1과 Boost에 있는 것들은 언제나 설명할 수 있을런지...)

혹시 틀린 부분이나 보완하고 싶으시 부분이 있으면 언제라도 알려주시고, 소프트웨어 관련된 저의 다른 글들도 참고로 읽어 보세요.