C++ 이야기 열다섯번째: 함수 객체 #3, 멤버 함수 어댑터

Posted at 2007. 6. 13. 07:38 // in S/W개발/C++ 이야기 // by 김윤수


C++ 이야기 열다섯번째입니다. 이번 글에서는 지난 글에서 밝힌 것처럼 멤버 함수를 함수 객체처럼 호출할 수 있게 해주는 mem_fun(), mem_fun_ref() 에 대해 설명드리도록 하겠습니다. 갈수록 C++ 이야기 시리즈가 인기가 없어지네요. 요즘은 도통 댓글도 안달리고... 그렇다고 제가 여기서 굴복할 사람이 아니죠. 제 밑천이 완전히 동날 때까지 쭉~ 가겠습니다.

그럼, mem_fun, mem_fun_ref 이 멤버 함수를 함수 객체처럼 호출할 수 있게 해준다고 말씀드렸는데, 어떻게 하면 멤버 함수를 함수 객체처럼 호출할 수 있을까요 ? 그냥 멤버 포인터를 알고리즘에 넘기면 되는 거 아니냐구요 ? 그럴까요 ? 그럼 한 번 해 보죠 뭐. 다음 같은 코드가 제대로 작동할까요 ?

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <functional>               // mem_fun(), mem_fun_ref() 정의됨

using namespace std;

class Person {
public:
  enum Sex { Male = 1, Female = 2 };

  Person(string name, int age, Sex sex):
    _name(name), _age(age), _sex(sex) {
  }

  // 이런 저런 멤버 함수들이 정의되어 있다고 칩시다

  void Print() {
    cout << _name << ": age = " << _age << ", sex = " <<
           (_sex == Male ? "Male" : "Female") << endl;
  }

private:
  string _name;
  int _age;
  Sex _sex;
};

int
main(void)
{
  vector<Person> v;

  v.push_back(Person(string("KYS"), 20, Person::Male));
  v.push_back(Person(string("JYS"), 18, Person::Female));
  v.push_back(Person(string("KYG"), 15, Person::Male));
  v.push_back(Person(string("KYJ"), 14, Person::Male));
  v.push_back(Person(string("KYE"), 11, Person::Female));

  for_each(v.begin(), v.end(), &Person::Print);
}

컴파일이 되시나요 ? 제 컴파일러는 for_each() 를 컴파일하는 와중에 다음과 같은 에러를 리턴하면서 코드를 토해 버리네요.

c:\program files\microsoft visual studio 8\vc\include\algorithm(29) : error C2064: term does not evaluate to a function taking 1 arguments
        c:\documents and settings\김윤수\my documents\visual studio 2005\projects\stl\mem_fun\mem_fun.cpp(40) : see reference to function template instantiation '_Fn1 std::for_each<std::_Vector_iterator<_Ty,_Alloc>,void(__thiscall Person::* )(void)>(_InIt,_InIt,_Fn1)' being compiled
        with
        [
            _Fn1=void (__thiscall Person::* )(void),
            _Ty=Person,
            _Alloc=std::allocator<Person>,
            _InIt=std::_Vector_iterator<Person,std::allocator<Person>>
        ]

역시나 템플릿 컴파일하다가 에러가 발생했을 때, 뱉어내는 컴파일러 에러는 참 해독하기가 힘드네요. 그래도 어떡하겠습니까 ? 어찌 됐든 해석을 해야 문제를 해결할 수 있죠. 우선 파란색으로 표시한 부분을 보니 우리가 넘겨준 멤버 함수는 인자가 하나도 없는데, 컴파일러는 인자가 하나인 함수를 찾나 봅니다. 왜 그런지 알아 보기 위해 <algorithm> 헤더에서 한 번 for_each()를 뒤져 보는 게 좋겠습니다.

template<class _InIt, class _Fn1>
inline _Fn1 for_each(_InIt _First, _InIt _Last, _Fn1 _Func)
{ // perform function for each element
  for (; _First != _Last; ++_First)
    _Func(*_First);          // 요 부분에 주목해 주세요

  return (_Func);
}


음... 컨테이너 각각의 요소에 대해 _Func(*_First) 와 같이 호출하네요. 그러니 당연히 인자가 없는 Person::Print() 는 호출될 수가 없겠네요. for_each() 로 넘긴건 void (*)(Person) 타입이 아니라 void (Person::*)(void) 이기 때문입니다. 멤버 포인터는 멤버가 속하는 클래스의 객체에 멤버 포인터 연산자(.*)를 적용해야만 호출할 수 있다는 건 아시죠 ? 예를 들자면, 이런식으로 말입니다.

int
main(void)
{
  Person p(string("KYS"), 20, Person::Male);
  // 멤버 포인터 person_mem_fun 선언
  void (Person::*person_mem_fun)() = &Person::Print;

  // 멤버 포인터를 통한 멤버 함수 호출. 함수 호출 연산자 '()' 의 우선 순위가
  // 멤버 포인터 연산자보다 높기 때문에 꼭 p.*person_mem_fun 을 괄호로
  // 둘러 싸야 컴파일 에러가 발생하지 않습니다.
  (p.*person_mem_fun)();
}

혹시 멤버 포인터에 대해 잘 모르시는 분은 "가서 젖이나 더 먹고 오세요" 라고 말하면 안되고......, 아래 참고 문헌을 참조하시기 바랍니다. ^^; 몇 몇 분이 순간 얼굴이 빨개 지셨네요.



각설하고 그렇다면, 함수 또는 함수 객체를 요구하는 알고리즘에 멤버 함수는 쓸 수가 없겠네요 ? 여기까지 설명을 보고는 "다른 알고리즘도 이런식으로 정의되어 있으면 멤버 함수를 호출할 수가 없겠네 ? 에이~ 그럼 객체 지향 방식으로 설계한 클래스하고는 쓰기가 어렵잖아 ?" 라고 생각하시는 분의 텔레파시가 저에게 전해져 오네요. 이렇게 생각하셨다면 STL을 정의한 사람과 C++ 표준 라이브러리를 표준화한 사람들의 능력을 너무 무시하는 처사입니다. 설마 객체 지향 패러다임을 지원하는 언어용 라이브러리에서 그 정도도 지원 안하려구요. 우리 다시 한 번 찬찬히 생각해 보죠.

그럼 어떻게 하면 알고리즘에서 멤버 함수를 호출하게 만들 수 있을까요 ? 힌트하나 드릴까요 ? C++ 이야기 열두번째: 함수 객체 #1에서 제가 함수 객체는 '()' 재정의한 객체로서 함수처럼 쓸 수 있다라고 얘기했던 것과 상태를 가질 수 있다라는 걸 상기해 보세요. 어떻게 아이디어가 떠 오르시나요 ? 돌 굴러가는 소리가 여기까지 들리네요. 예~ 답을 찾으셨다구요 ? 멤버 함수에 대한 포인터를 상태로 기억해 놓았다가 함수 호출 연산자가 호출되면 객체에 멤버 함수 포인터를 호출하도록 하면 된다는 말씀이시죠 ?

// 인자가 없는 멤버 함수 포인터 타입을 정의
typedef void (Person::*MemFunPtr)();

class MyMemFun {
private:
  MemFunPtr _mfp;

public:
  // 생성자에서 멤버 함수 포인터를 상태로 기억해 놓음
  MyMemFun(MemFunPtr mfp): _mfp(mfp) {
  }

  // 호출할 때는 객체에 멤버 연산자를 적용해서 호출
  void operator() (Person p) {
    (p.*_mfp)();
  }
};

// main() 함수를 다음과 같이 수정합니다
int
main(void)
{
  vector<Person> v;

  v.push_back(Person(string("KYS"), 20, Person::Male));
  v.push_back(Person(string("JYS"), 18, Person::Female));
  v.push_back(Person(string("KYG"), 15, Person::Male));
  v.push_back(Person(string("KYJ"), 14, Person::Male));
  v.push_back(Person(string("KYE"), 11, Person::Female));

  for_each(v.begin(), v.end(), MyMemFun(&Person::Print));
}

이제 컴파일 한 번 해 볼까요 ? 왠지 가슴이 막 두근 두근 하네요. 과연 여러분이 제시한 답이 맞을까요 ? 예스~ 아무런 에러 없이 컴파일이 잘 되네요. 그럼 이제 실행해 볼까요 ? 오~! 예~! 실행도 아무런 문제 없이 잘 되네요. 역시 C++ 이야기 시리즈 독자분들은 능력이 출중하시네요.

그렇지만, 방심은 금물. 아직 넘어야할 산이 하나 더 있습니다. MyMemFun 클래스를 템플릿화시켜보죠. 일단 MyMemFun 클래스는 잘 도는 게 확인됐으니 타입에 해당하는 부분만 템플릿 인자로 바꾸면 되겠네요. MyMemFun 클래스에서 달라질 수 있는 타입은 멤버 함수의 리턴 타입과 클래스명이네요. 그렇다면, 다음과 같이 하면 템플릿화 시킬 수 있겠습니다.

// 리턴 타입과 클래스명을 템플릿 인자로
template <typename R, class C>
class MyMemFun {
public:
  // 멤버 함수 포인터 타입을 선언
  typedef R (C::*MemFunPtrType)();

  // 생성자에서 멤버 함수 포인터를 상태로 기억해 놓음
  MyMemFun(MemFunPtrType mfp): _mfp(mfp) {
  }

  // 호출할 때는 객체에 멤버 연산자를 적용해서 호출
  void operator() (Person p) {
    (p.*_mfp)();
  }

private:
  MemFunPtrType _mfp;
};


// main() 함수는 다음과 같이 수정
int
main(void)
{
  ......
  for_each(v.begin(), v.end(), MyMemFun<void, Person>(&Person::Print));
}


실제 C++ 표준 라이브러리의 멤버 함수 어댑터인 mem_fun, mem_fun_ref 는 위에서 제시한 것과 거의 동일한 아이디어를 사용해서 구현되어 있습니다. 약간 고급 프로그래밍 기법을 사용해서 템플릿 인자를 명시하지 않아도 되도록 하고 있습니다. 그래서 mem_fun_ref() 을 쓸 경우 다음과 같이 작성하시면 됩니다.

for_each(v.begin(), v.end(), mem_fun_ref(&Person::Print));

mem_fun() 은 컨테이너의 요소가 포인터일 경우 사용하고, mem_fun_ref() 는 객체일 경우 사용합니다. 두 개 이름이 조금 헤깔리게 되어 있으니 잘 구별해서 사용하셔야 겠습니다. 그리고, 또 한가지 주의하셔야할 점은 mem_fun() 과 mem_fun_ref() 는 인자가 하나인 멤버 함수까지만 지원할 수 있다는 것입니다.

음... 이렇게 해서 표준 라이브러리에 있는 함수 객체에 대한 내용은 얼추 다 설명드린 것 같습니다. 함수 객체 #1 에서는 함수 객체가 무엇인지에서 시작해서 미리 정의되어 있는 함수 객체 및 binder 에 대해 이야기 했고, 함수 객체 #2 에서는 조건자와 ptr_fun() 함수 어댑터에 대해 이야기 했습니다.

이제 다음 글에서는 TR1 과 Boost 의 bind(), mem_fn(), function(), lambda() 등에 대해 설명 드리도록 하겠습니다. 혹시 틀린 부분이나 보완하고 싶으신 부분이 있으면 언제라도 알려주시고, 소프트웨어 관련된 저의 다른 글들도 참고로 읽어 보세요.

참고 문헌

[1] WinApi 의 33-3.멤버 포인터 연산자
  가.멤버 포인터 변수
  나.멤버 포인터 연산자의 활용
  다.멤버 포인터의 특징

[2] Bjarne Stroustrup 저, 곽용재 역, The C++ Programming Language (특별판) 번역서, 15.5 멤버에 대한 포인터(p560 ~ p563)

[3] Nicolai M. Josuttis 저, 문정환/김희준 공역, C++ Standard Library (Tutorial&Reference) 8장 STL 함수-객체

[4] Scott Meyers 저, 곽용재 역, Effective STL, 항목 40, 41

[5] Lars Haende 저, Alones님 역, [번역 작업] The Function Pointer Tutorial