C++ 이야기 열두번째: 함수 객체 #1

Posted at 2007.05.31 16:25 // in S/W개발/C++ 이야기 // by 김윤수


C++ 이야기 열두번째입니다. 이번에도 역시 어떤 글을 쓸까 고민 고민했습니다. 갈수록 제 내공이 바닥나다 보니 퍼낼 밑천이 없어서... ㅎㅎㅎ 그러다가 C++ 표준 함수 객체부터 시작해서 Boost 에 포함되어 있는 lambda 템플릿을 소개하기로 맘을 먹었습니다. 왜냐구요 ? 그건 다음 글을 읽으면서 한 번 느껴보시죠.

그럼 제일 먼저 함수 객체가 무엇인지 알아 보죠. 이 글을 읽으시는 독자 대부분이 함수 객체가 무엇인지를 대강 아실듯 하지만... 그래도 기본을 다시 짚어 본다는 의미에서 설명해 보도록 하겠습니다.

'함수 객체란 무엇인가 ?' 그 이름에서 뜻을 유추해 본다면 함수처럼 작동하는 객체다라고 말씀드릴 수 있겠네요. 그럼 일단은 객체라고 했으니, class 나 struct 로 정의를 해야하는 것 같은데... 함수처럼 작동하게 하려면 어떻게 하면 될까요 ? 이 질문에 답을 할 수 있으려면 '함수처럼 작동한다'라는 것이 어떤 것을 뜻하는 것인지를 짚어 봐야 할 것입니다. C++에서 "함수!" 하면 가장 먼저 어떤 것이 떠오르시나요 ? 함수를 정의하는 것과 함수를 호출하는 것 이 두 가지 떠오르실 겁니다. (아니면 말구요 ^_^)

// 다음은 함수 정의
void foo()
{
  statements;
}

// 다음은 함수 호출
// C++에서는 어떤 식별자에 '()' 가 따라오면 함수 호출로 인식합니다.
foo();

그럼 다시 질문 들어갑니다. 함수처럼 작동하게 하려면 어떻게 하면 될까요 ? (자! 잠깐 글에서 눈을 떼시고 한 번 생각해 보세요~)
.
.
.
.
.
.
제가 힌트 하나 드릴까요 ? C++ 에서 '+', '-', '*', '<<' 등 대부분의 연산자를 재정의하실 수 있는 거 기억하시나요 ? 그걸 이용하시면 됩니다.
.
.
.
.
.
.
그래도 잘 모르시겠다구요 ? 그럼 하나더 힌트를 드리죠. 이거 가르쳐 드리면 거의 답을 가르쳐 드리는 건데... C++에서 '()' 가 함수 호출 연산자라는 거 아시나요 ?
.
.
.
.
.
.
아하! 그렇죠. 클래스 정의 안에서 함수 호출 연산자 '()'를 재정의하시면 되는 겁니다. 이해가 안되신다면 이론을 백번 설명하는 것보다 간단한 예가 훨씬 낫죠. 자~ 예를 한 번 들어볼까요 ? 다음은 합계와 평균을 구해주는 함수 객체입니다.

class Statistics {
private:
  int count;
  int sum;
  double average;

public:
  Statistics (): count(0), sum(0), average(0.0) {}

  // '()' 연산자를 재정의. 이게 핵심입니다.
  int operator () (int i) {
    sum += i;
    count++;
   
return sum;
  }

  int N() {
    return count;
  }

  int SUM() {
    return sum;
  }

  double AVG() {
    average = static_cast<double>(sum)/count;
    return average;
  }
};

이 함수 객체를 사용할 때는 다음과 같이 할 수 있습니다.

int
main(void)
{
  Statistics s;

 
 for (int i = 1; i <= 100; i++) {
    s(i);  // 함수 객체 호출. 그냥 함수 호출과 구분이 안되죠 ?
  }

 
cout << "sum = " << s.SUM() << ", avg = " << s.AVG() << endl;

 
return 0;
}

어때요 ? 함수 객체가 함수보다 좋다는 감이 오시나요 ? 전 젤 처음에 함수 객체라는 개념을 봤을 때 "요거 물건이네..."라는 감이 오더군요. 제가 생각하기에 함수 객체의 가장 큰 장점은 함수는 함수인데 상태를 가질 수 있는 함수이다라는 것이더군요. 위에 보시는 Statistics 의 예처럼 상태를 가질 수 있다는 특성 때문에 보통 함수보다는 할 수 있는 일이 훨씬 다양해 지게 되거든요. 거기에다가 함수가 쓰일 수 있는 곳이라면 어디에든 쓰일 수가 있게 되는 것이죠. 또 다른 예로, 함수 객체를 써서 구구단을 한 번 찍어 볼까요 ?

먼저 인자로 주어진 수에 생성시에 정해진 수를 곱하는 함수 객체를 작성해 보겠습니다.

class PrintTimes {
private:
  int _n;

public:
  PrintTimes(int n): _n(n) {}
  void operator() (int i) {
   cout << _n << " * " << i << " = " << i * _n << endl;
  }
};

이 함수를 써서 구구단을 출력하려면 다음과 같이 하면 되겠죠 ?

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

  for (int i = 1; i < 10; i++) {
    v.push_back(i);
  }

  for (vector<int>::iterator ii = ++v.begin(); // 2단부터 출력
      ii != v.end();
      ++ii) {
    cout << "<< " << *ii << "단 >>" << endl;
    // PrintTimes(*ii) 가 호출되면서 _n 이 차례로 2 ~ 9로 초기화됩니다.
    // 그리고 for_each() 알고리즘을 사용합니다.
    for_each(v.begin(), v.end(), PrintTimes(*ii));
  }

  return 0;
}



위 코드에서 설명한 대로 PrintTimes(*ii) 와 같이 호출하면 PrintTimes 함수 객체의 임시 객체가 생성되면서 _n 멤버 변수가 각 단계별로 2 ~ 9로 설정됩니다. 그러면, 구구단의 2단부터 9단까지를 for_each() 알고리즘을 사용해서 출력할 수 있게 되는 것이지요. 이런식으로 함수 객체는 실행시에 그 행동 방식을 바꿀 수가 있게 됩니다. 이번엔 다른 식으로 이 문제를 해결해 볼까요 ? 이번엔 PrintTimes 함수 객체를 둘로 나누어 m x n = mn 으로 찍어주는 함수 객체와 m 을 고정시켜 주는 객체로 나누어 보도록 하겠습니다. 다음과 같이 말이죠.

// m x n = mn 을 출력하는 함수 객체
struct PrintMultiplication {
  void operator() (int m, int n) {
    cout << m << " * " << n << " = " << m * n << endl;
  }
};

// m 을 고정시켜주는 객체
class Binder {
public:
  // 호출할 함수 객체와 고정할 인자를 기억해 놓습니다
  Binder(const PrintMultiplication& op, int arg): _op(op), _arg(arg) {}
  // 첫번째 인자를 고정된 값으로 호출합니다
  void operator() (int i) {
    _op(_arg, i);
  }

private:
  PrintMultiplication _op;    // 호출할 함수 객체
  int _arg;                   // 고정할 인자의 값
};

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

  for (int i = 1; i < 10; i++) {
    v.push_back(i);
  }

  for (vector<int>::iterator ii = ++v.begin(); // 2단부터 출력
      ii != v.end();
      ++ii) {
    cout << "<< " << *ii << "단 >>" << endl;
    // Binder() 가 호출되면서 PrintMultiplication() 함수 객체의 첫번째
    // 인자가 *ii 로 for_each() 알고리즘이 수행되는 동안 고정됩니다
    for_each(v.begin(), v.end(), Binder(PrintMultiplication(), *ii);
  }

  return 0;
}

이해가 되시나요 ? 위 코드만 보면 왜 괜히 PrintMultiplication 과 Binder 를 굳이 나누었을까가 이해되지 않겠지만 Binder를 템플릿화시키면 그 장점이 확연히 드러나게 됩니다. Binder 를 템플릿화시키면 다음과 같이 작성할 수 있을 것입니다.

template <class OP, typename ARG>
class Binder {
public:
  // 호출할 함수 객체와 고정할 인자를 기억해 놓습니다
  Binder(const OP& op, ARG arg): _op(op), _arg(arg) {}
  // 첫번째 인자를 고정된 값으로 호출합니다
  void operator() (int i) {
    _op(_arg, i);
  }

private:
  OP _op;                     // 호출할 함수 객체
  ARG _arg;                   // 고정할 인자의 값
}


위와 같이 작성하면 ARG 타입의 인자를 갖는 임의의 OP 타입 함수 객체에 대해 인자 binding이 가능하게 됩니다. 사실 C++ 표준 라이브러리에 정의된 bind1st, bind2nd 의 배경이 되는 아이디어도 위 코드에서 정의한 Binder 클래스와 정확히 일치합니다. 게다가 C++ 표준 라이브러리에서는 기본적인 연산에 대해서 이미 함수 객체로 정의해 놓고 있습니다. 다음은 미리 정의된 함수 객체들입니다.

표현식

효과

negate<type>()

- 인자

plus<type>()

인자1 + 인자2

minus<type>()

인자1 – 인자2

multiplies<type>()

인자1 * 인자2

divides<type>()

인자1 / 인자2

modules<type>()

인자1 % 인자2

equal_to<type>()

인자1 == 인자2

not_equal_to<type>()

인자1 != 인자2

less<type>()

인자1 < 인자2

greater<type>()

인자1 > 인자2

less_equal<type>()

인자1 <= 인자2

greater_equal<type>()

인자1 >= 인자2

logical_not<type>()

! 인자

logical_and<type>()

인자1 && 인자2

logical_or<type>()

인자1 || 인자

bind1st(op,value)

op(value, 인자)

bind2nd(op,value)

op(인자, value)

not1(op)

!op(인자)

not2(op)

!op(인자1,인자2)


이러한 함수 객체들을 사용하면 다양한 작업을 손쉽게 할 수 있습니다. 예를 들어, vector에 있는 모든 수에 3씩 더하고 싶다 그런 경우에는 transform() 알고리즘을 사용해서 다음과 같이 할 수 있습니다.

// 아래의 Print 템플릿 함수는 다음과 같이 정의되어 있습니다.
template <typename T>
struct Print {
  void operator() (T v) {
    cout << v << " ";
  }
};

for_each(v.begin(), v.end(), Print<int>());
cout << "\n";
transform(v.begin(), v.end(), v.begin(), bind2nd(plus<int>(), 3));
for_each(v.begin(), v.end(), Print<int>());
cout << "\n";

plus<int>() 함수 객체의 두번째 인자를 3으로 고정하여 원하는 결과를 얻어낼 수 있습니다. 다음은 위 코드를 실행한 결과입니다.

1 2 3 4 5 6 7 8 9
4 5 6 7 8 9 10 11 12

마지막으로 한 가지만 더 예를 들고 이번 글을 마칠까 합니다.

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

위 코드에서 bind2nd(modulus<int>(), 2) 는 2로 나눈 나머지를 리턴하게 되므로 홀수에 대해서는 1을 리턴하게 될 것입니다. 그리고, 그 결과에 not1이 수행되므로 짝수가 참이 되어 결국 위 코드는 첫번째 짝수 위치를 찾게 될 것입니다. 마지막으로 다음 두 가지를 기억해 두시면 좋을 것 같습니다. find_if() 알고리즘에 대한 것은 Alones님의 std::find_if 사용해보기 를 참고하세요

1. 함수 객체는 함수처럼 동작하는 객체이다.
2. 함수 객체는 '()' 연산자를 재정의해서 정의할 수 있다.

역시나 이번에도 생각보다 글이 많이 길어졌습니다. 항상 시작할 때는 간단하게 써야겠다고 다짐을 하는데도, 제 성격상 어쩔 수 없이 길어지나 봅니다. 너무 길어지면 지루할테니 이쯤에서 이번 글을 마무리하고 다음에도 함수 객체에 대해 못다한 이야기를 이어가렵니다. 혹시 틀린 부분이나 보완하고 싶으시 부분이 있으면 언제라도 알려주시고, 소프트웨어 관련된 저의 다른 글들도 참고로 읽어 보세요.


신고
  1. alones

    2007.06.01 09:40 신고 [수정/삭제] [답글]

    안녕하세요. 글 잘 읽었습니다.

    함수 객체에 대한 예제 중 find_if를 예로 드셨는데
    Predicate class를 만들 때 함수 객체 (operator() )를 재정의 한 것에 대한 글이 있어서 trackback을 걸었습니다.

  2. 김윤수

    2007.06.01 17:33 신고 [수정/삭제] [답글]

    유용한 링크 걸어주셔서 감사합니다. ^^

  3. 김규만

    2007.07.06 15:45 신고 [수정/삭제] [답글]

    boost라는게 대단한 라이브러리같군요.
    이젠 functional programming 문제에 자주 출제되는 lambda function 까지 구현을 하다니요. 갑자기 지지난 여름 졸업셤의 악몽이 떠오릅니다. ML로 lambda function 구현하고 설명하라. 흑. 재미있어요.

  4. prism

    2008.01.13 17:20 신고 [수정/삭제] [답글]

    STL이 이런 원리였다니 진짜 대단하네요. 알려주셔서 정말 감사합니다^^

  5. 바다21

    2010.06.14 19:09 신고 [수정/삭제] [답글]

    TC++PL 을 읽으면서(번역본이라서 그런가..) 함수객체에 대한 설명을 읽고도 도무지 무슨 말인지 몰랐는데, 이렇게 핵심을 콕 찝어주시니 정말 감사합니다. 아주 속이 다 후련하군요. ㅎㅎㅎ

  6. Qrium

    2010.10.18 22:18 신고 [수정/삭제] [답글]

    잘 보고 갑니다. ~ 쉽게 설명해 주셨네요. !

  7. 나그네

    2013.05.13 01:50 신고 [수정/삭제] [답글]

    stl은 맵이나 벡터같은것만 썼었는데 오늘 함수객체에 대해 배워가네요.
    람다함수를 검색하다가 함수객체,함수포인터와 다르다고 하길래 모르는 부분인 함수객체에 대해서 찾아보다가 여기까지 오게되었습니다.

  8. koko

    2016.10.16 03:50 신고 [수정/삭제] [답글]

    펑터에 대한 글들을 찾아가 이 사이트에 우연히 들어오게 되었는데 정리를 너무 잘 해놓으신것 같아요 !
    펑터 외에도 다양한 글들을 올려놓으셔서 다 읽어보려구 즐겨찾기 하고 갑니다 :)
    덕분에 풍요로운 지식을 쌓을수 있을 것 같아요 감사합니다 :) !

댓글을 남겨주세요.