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

Posted at 2007.06.12 15: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

신고
  1. alones

    2007.06.12 16:58 신고 [수정/삭제] [답글]

    안녕하세요. 좋은 글 잘 읽었습니다.
    사실 14번째 이야기를 구글 리더에 별표로 마킹해두고 오늘 아침에 읽고 예제 코드를 디버깅도 해보았습니다. Visual C++ 6.0의 STL 코드 쪽도 좀 보구요.

    근데 놀라운 것은 ^^;;;
    14번째 글 마지막에 다음 글이 class 멤버 함수의 호출에 관계 된 내용을 쓰신다고 해서
    예전에 function pointer (class와 functor도 포함하는)에 관한 글을 번역하고 정리한게 있어서 trackback을 건다는게
    이 블로그 타이틀을 클릭하고 키보드 'End' 키를 누르고 트랙백을 걸었는데
    (구글 리더에 제가 읽고 있을 때만 해도 새글이 없어서 14번째가 최신인줄 알고 ...)
    15번째 글에 걸렸군요. ^^

    아무튼 15번째 글은 또 아껴뒀다가 쉬는 시간에 다시 또 읽어 봐야겠습니다.

    ※14번째 글 잘 읽었습니다~~~
    Predicate의 내부 상태 변은 remove_if와 같이 (말씀하신 것 처럼)내부적으로 remove_copy_if를 쓴 다는 사실을 모르거나 잊고 있을 때 더욱 큰 재앙을 읽으킬 것 같습니다. ^^

    그리고 후반부의 Predicate를 function으로 넘기는 것은 (그런 방법으로 parameter가 2개이고 return이 reverse로 쓰여도 가능하군요 ^^)
    그 경우면 Predicate class를 만드는게 좋을 것 같습니다. 물론 이런 것도 된다를 보여주시렬고 예를 들었겠지만.

    아무튼 좋은 글들 잘 읽고 있습니다.
    좋은 하루 되십시오~~

  2. alones

    2007.06.12 17:04 신고 [수정/삭제] [답글]

    허걱..

    첫 번째 대리자 관련은 잘 못 보낸 것입니다. ^^;;;

    아무튼 좋은 하루 되세요

  3. 김규만

    2007.07.09 11:05 신고 [수정/삭제] [답글]

    아~ 한국말이 이해가 안가서 몇번을 읽었어요. 이거 여기 살면서 언어 능력이 점점 더 퇴화되는 듯. 새로운 내용을 알아가니 재미있네요. 나도 가서 젖 좀 먹고 와야겠다~~. ^^ 근데, 윤수씨도 이미 잘 알겠지만, std::string 타입으로 인자를 넘길땐 const std::string& 로 프로토타입을 정의하면 const char*도 그냥 쓸 수 있지요. 위에서

    Person::Person(string name, int age, Sex sex)

    요 부분을

    Person::Person(const string& name, int age, Sex sex)

    로 선언하면 나중에 main 본문에서

    v.push_back(Person("KYS", 20, Person::Male));

    처럼 쓸 수 있어서 편하더라구요.
    뭐 다 알고 계신거겠지만, 혹시나 해서 남깁니다.

    • 김윤수

      2007.07.09 19:20 신고 [수정/삭제]

      김규만씨 오셨군요. 제가 쓴 글이 그렇게 어려운가요 ? 저는 쉽게 써 본다고 쓴 건데도 역시 어려운가 보네요. 어떤 부분이 제일 막혀요 ?

      본문의 코드를 김규만씨가 말한 대로 바꾸면 더 깔끔하겠네요.

  4. 정희철

    2010.09.14 23:00 신고 [수정/삭제] [답글]

    for_each에서 어떻게 하면 클래스의 맴버함수를 넣을수 있을까?를 고민하다가 시간이 없다는 핑계로 그냥 함수객체 하나 더 만들어 사용해 버렸는데.. 윤수님 글을 보고 배워갑니다.. stl 알고리즘들과 더 친해 질 수 있을 것 같네요..

  5. 빠숑

    2012.10.10 01:04 신고 [수정/삭제] [답글]

    이거 여기서 한수 배워서 갑니다.. 벌써 5년도 더된 포스트인데 지금 저한테는 무척이나 새롭고 재미있네요~~ 계속해서 들려서 배워 갈께요 ㅎㅎ

  6. 안준혁

    2013.08.12 00:56 신고 [수정/삭제] [답글]

    우연히 찾은 사이트인데;; 엄청 도움이 되고 있어요!! 감사합니다!!~

댓글을 남겨주세요.