////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////


함수객체란
  함수 처럼 동작 하는 객체
 *사용법
   ㄱ.'()' 연산자 사용.
   ㄴ. '()'연산자는 함수 호출연산자.
   함수 호출 연산자 ()를 재정의 한다. 

두 수의 곱을 출력하는 함수 객체

  1. class CFuncObject
  2. {
  3. public :
  4.     CFuncObject( int n ) : m_n(n) {}
  5.     virtual~ CFuncObject() {}
  6.  
  7.     void operator () (int i)
  8.     {
  9.         std::cout<< m_n << "*" << i << "=" << m_n*<<std::endl;
  10.     }
  11.  
  12. private :
  13.     int m_n;
  14. };


위의 함수 객체를 둘로 나누어 출력을 담당하는 함수 객체, 함수객체와 인자 하나를 고정 시켜 주는 개체를 나누어 코딩을 해보자.

  1. using namespace std;
  2.  
  3. struct PrintMultiplication
  4. {  
  5.   void operator() (int m, int n)
  6.   {
  7.     cout << m << " * " << n << " = " << m * n << endl;
  8.   }
  9. };
  10.  
  11. class Binder
  12. {
  13. public:
  14. // 호출할 함수 객체와 고정할 인자를 기억해 놓습니다  
  15.   Binder(const PrintMultiplication& op, int arg): _op(op), _arg(arg) {}
  16.  // 첫번째 인자를 고정된 값으로 호출합니다
  17.   void operator() (int i)
  18.   {
  19.     _op(_arg, i);
  20.   }
  21. private:
  22.   PrintMultiplication _op;    //호출할 함수 객체
  23.   int _arg;                  // 고정할 인자의 값
  24. };
  25.  
  26. int main(void)
  27. {
  28.   vector<int> v;
  29.  
  30.   for (int i = 1; i < 10; i++)
  31.   {
  32.     v.push_back(i);
  33.   }
  34.  
  35.   for (vector<int>::iterator ii = ++v.begin();
  36.       ii != v.end(); ++ii)
  37.   {
  38.     cout << "<< " << *ii << "단 >>" << endl;
  39.  
    //PrintTimes(*ii)가 호출 되면서 _n이 차례로 2~9로 초기화 된다.
    // 그리고 for_each() 알고 리즘을 사용한다. 
  40.     for_each(v.begin(), v.end(), Binder(PrintMultiplication()*ii));
  41.   }
  42.  
  43.   system("pause");
  44.  
  45.   return 0;
  46. }


언뜻 보았을 때는 기능을 왜 굳이 둘로 나누었는지 모르겠지만,  Binder를 템플릿화 시켜서 사용하면, 재사용성이라는 장점이 부각되므로 꽤나 쓸만해 진다.

  1. template <class OP, typename ARG>
  2. class Binder {
  3. public:
  4.   Binder(const OP& op, ARG arg): _op(op), _arg(arg) {}
  5.  
  6.   void operator() (int i) {
  7.     _op(_arg, i);
  8.   }
  9.  
  10. private:
  11.   OP _op;                
  12.   ARG _arg;                  
  13. }

 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)


함수 객체의 장점

1. 함수에 특정 상태, 속성을 넣을 수 있게 되었음.
2. 1번의 경우 덕분에 보다 OOP적인 코딩이 가능해짐.
3. 함수를 동적 메모리 할당해서 쓰고 버릴 수 있음. (객체이기 때문에 가능한 것!)
4. 템플릿과 함수객체를 혼합해서 사용하며느 보다 다양한 함수 객체가 가능해짐.
5. 일반 함수보다 빠르다고 함. (테스트 안해봄...ㅡㅡ;;)

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////


1. 함수 객체

1) STL 알고리즘의 모호성

 STL 알고리즘들은 전역함수가 처리하며 문제를 풀기위한 반복자 구간, 검색 대상 ,채울 값 따위의

정보들이 함수의 인자로 전달된다. 알고리즘 함수들은 입력된 정보를 바탕으로 알아서 동작하지만 어떤 함수들은

내부에서 모든 동작을 다 처리하지 않거나 할 수 없는 경우가 있다.  즉 검색하는 값이 정확하게 어떠한 조건인지, 정렬을 위해 요소를 비교할때 어떤 방식으로 비교 할 것인지를 함수가 마음대로 결정할 수 없다.

 

2) 모호성의 해결 : 함수 객체

이때 함수에게 좀더 구체적인 처리 방식을 지정하기 위해 사용자가 마리 만들어 놓은 함수 객체를 전달한다. 알고리즘 함수는 동작중에 사용자의 개입이 필요한 부분에 대해서 함수 객체를 호출하여 의사를 결정한다.

 

3) 동작방식

벡터의 순회의 경우 결과를 확인하기 위해 직접 순회하면서 벡터의 요소를 일일이 출력한다. 이때 이런 순회는 반복적으로 루프를 구성해야 하므로 번거롭다. 이때 순회를 담당하는 함수 객체를 사용하여 이러한 처리를 대신 시킬수 가 있다.  

  UniOp for_each(init first, Init last , UniOp op)

예) 함수 포인터를 이용한 함수 객체 흉내

 

  : 함수 포인터

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;

void print
{
 printf("%d\n",a);
};

void main()
{
 int ari[]= { 2,3,5,1,9};
 vector<int> vi(&ari[0],&ari[5]);


 sort(vi.begin(), vi.end());

 for_each(vi.begin(),vi.end(),print());     // 함수 포인터를 객체로 
}

 

예) 함수 객체의 사용

STL 의 함수 객체는 함수 포인터에만 국한 되는 것이 아니라함수를 흉내낼 수 있는 모든 객체일 수 있다는 점이 다르다.함수 객체는 함수 호출 연산자인 ()을 오버로딩한 객체를 의미한다.  이 연산자를 통해 마치 함수를 호출하듯이 객체를 호출할 수 있다.  

 

  : iterarray

struct print
{
 void operator()(int a) const
 {
  printf("%d\n",a);
 }
};

void main()
{
 int ari[]= { 2,3,5,1,9};
 vector<int> vi(&ari[0],&ari[5]);


 sort(vi.begin(), vi.end());

 for_each(vi.begin(),vi.end(),print());        // 함수 객체의 사용 
}

 

 

4) 함수 객체의 특징 1 : 멤버 변수

 

 함수 객체는 말 그대로 객체이기 때문에 함수 연산자() 뿐만 아니라 처리에 필요한 멤버들을 추가로 가질 수 있다.  연산중에 필요한 변수가 있으면 멤버로 만들 수 있고 필요한 동작이 있다면 멤버 함수로 가질 수 있다.

 

  : 객체함수 멤버

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;

struct accum
{
 int sum;
 accum() { sum=0; }
 void operator()(int a)
 {
  sum += a;
 }
};


void main()
{
 int ari[]= { 2,3,5,1,9};
 vector<int> vi(&ari[0],&ari[5]);

 sort(vi.begin(), vi.end());


 accum f;
 f = for_each(vi.begin(),vi.end(),f);        // 함수 객체의 사용

 printf("총합 : %d",f.sum);

}


 

 

5) 함수 객체의 특징 2 : 생성자, 멤버 함수 

 

 멤버뿐만 아니라 멤버 함수도 가질 수 있으며 생성자와 파괴자도 활용할 수가 있다.  특히 생성자는 멤버의 값을 원하는 대로 초기화 할 수 있다는 점에서 함수 객체에 대하여서도 실용성이 높다.

 

 

  : 생성자

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;

 

struct print
{
 string mes;
 print(string &m) : mes(m){}      // 생성자의 활용 
 void operator()(int a) const
 {
  cout << mes;
  printf("%d\n",a);
 }
};

 

void main()
{
 int ari[] = { 2,8,5,1,9};
 vector<int> vi(&ari[0],&ari[5]);
    
 sort(vi.begin(),vi.end());
    
 for_each(vi.begin(),vi.end(),print(string("요소값은")));
 for_each(vi.begin(),vi.end(),print(string("숫자는")));

}


 

 

 

2. 알고리즘의 변형

1) 함수객체의 변형

 

for_each 함수는 순회 중에 할 일을 결정하기 위해 반드시 함수 객체를 부르도록 되어 있다.

순회만 할 바에야 for_each를 부를 필요가 없으므로 for_each에게 함수 객체는 필수적인 존재라고 할 수 있다. 이처럼 함수 객체를 명시적으로 요구하는 알고리즘도 있고 필요할 때만 함수 객체를 옵션으로 받는 알고리즘도 있다.

컨테이너에서 값을 검색하는 find는 순회중의 반복자 값과 세 번째 인수로 지정한 값(val)을 == 연산자로 비교하여 정확하게 일치하는 요소를 찾아낸다. 그런데 때로는==로 정확한 일치를 검색하는 것이 아니라 사용자가 정의하는 방식으로 검색할 요소를 골라야 하는 경우도 있다. 이때 함수 객체로 요소를 직접 비교할 수 있는데 이런 함수는 보통 원래 함수의 이름 끝에 _if가 붙는다. find의 함수 객체 버전은 다음과 같다.

 

InIt find_if(InIt first, InIt last, UniPred F);

 

세 번째 인수 F는 () 연산자를 오버로딩하는 함수 객체이며 요소값 하나를 인수로 전달받아 이 값이 원하는 조건이 맞는지 검사하여 bool형을 리턴한다. 찾는 조건에 맞으면 true를 리턴하고 아니면 false를 리턴할 것이다.

 

예)

 

  : find_if

#include <iostream>

#include <string>

#include <vector>

#include <algorithm>

using namespace std;

 

struct IsKim {

     bool operator()(string name) const {

          return (strncmp(name.c_str(),"김",2)==0);

     }

};

 

void main()

{

     string names[]={"김유신","이순신","성삼문","장보고","조광조",

          "신숙주","김홍도","정도전","이성계","정몽주"};

     vector<string> vs(&names[0],&names[10]);

 

     vector<string>::iterator it;

     it=find_if(vs.begin(),vs.end(),IsKim());

 

     if (it==vs.end()) {

          cout << "없다." << endl;

     } else {

          cout << *it << "이(가) 있다." << endl;

     }

}

 

 

2) 함수 객체의 장점

 

함수 객체는 고정된 의미를 가지는 알고리즘에 유연성을 부여하여 활용도를 대폭적으로 향상시킨다비교 조건을 직접 작성할 수 있으므로 정확하게 같은 것만 검색하는 것이 아니라 사용자가 원하는 어떤 조건으로도 검색할 수 있다.

사원 명부 컨테이너에서 직급이 과장 이상이고 나이는 45 ~ 49세 사이이며 가불을 한 적이 있고 입사한지 10년 이상 되었고 자택을 소유한 남자 사원을 검색하는 정도의 복잡한 동작까지도 가능해진다.

 

find는 템플릿으로 되어 있으므로 임의의 컨테이너에 대해 검색을 수행할 수 있는 일반성을 가진다. 검색 대상을 템플릿 인수로 전달받으므로 인수로 검색 대상을 지정할 수 있다. find_if는 여기에 비교 방식까지도 인수로 전달받아 검색 조건이 무엇인가까지도 사용자가 지정할 수 있다. 그래서 find보다 find_if가 훨씬 더 일반적이다.

find뿐만 아니라 대부분의 STL 알고리즘은 함수 객체를 인수로 취하는 버전이 있다. 정렬, 대체, 병합, 계산 등에 사용자가 개입할 여지가 많이 남겨져 있어 STL이 제공하는 기능대로만 사용하지 않아도 된다. 그래서 60개밖에 안되는 알고리즘으로도 엄청나게 많은 일을 처리할 수 있는 것이다.

 

 

3. 정의된 함수 객체

1) 정의됨 함수 객체

 

함수 객체는 통상 () 연산자 하나만 정의하고 그나마도 동작이 간단해 길이가 아주 짧다.

이런 짧은 클래스도 직접 선언해서 쓰자면 번거로운데 그래서STL은 자주 사용할만한 연산에 대해 미리 함수 객체를 정의하고 있다. 이런 객체들은 별다른 정의없이 그냥 사용하기만 하면 된다. 대표적으로 가장 간단한 함수 객체인 plus를 보자. 더할 피연산자의 타입 T를 인수로 받아들이는 클래스 템플릿이다.

 

struct plus : public binary_function<T, T, T> {

     T operator()(const T& x, const T& y) const { return (x+y); }

};

 

이 선언문에서 : public 이하의 내용은 다음 항의 주제이며 꼭 없어도 상관없으므로 잠시 무시하도록 하자. 본체 내용도 아주 쉬운데 T형의 x, y를 전달받아 x+y를 리턴한다. T가 아주 뚱뚱한 클래스일 수도 있으므로 값이 아닌 레퍼런스로 전달받고 피연산자를 상수로 취급한다는 정도 외에는 특별할 것도 없다. T 가 int라면 결국 a, b를 받아 a+b를 리턴하는 동작을 하는 함수 객체이다. 간단한 사용예를 보자.

 

  : plus

#include <iostream>

#include <functional>

using namespace std;

 

void main()

{

     int a=1,b=2;

     int c=plus<int>()(a,b);

     cout << c << endl;

}

 

함수 객체와 그 지원 매크로, 타입 등은 모두 functional 헤더 파일에 정의되어 있으므로 이 헤더 파일을 인클루드해야 한다. main에서 정수형 변수 a와 b를 선언하고 plus 객체의 함수 ()를 호출하여 두 정수의 합을 계산했다. 여기서 plus<int>() 구문이 조금 복잡해 보이는데 앞에서 설명했다시피 디폴트 생성자 호출문이며 임시 객체를 생성한다. 생성된 임시객체로부터 ()연산자 함수를 호출하되 인수로 a, b를 넘긴 것이다. 좀 쉽게 풀어쓰면 다음 두 줄이 된다.

 

plus<int> P;

int c=P(a,b);

 

plus 클래스 템플릿으로부터 plus<int> 타입의 클래스를 구체화하고 이 클래스 타입의 객체 P를 선언한다. 그리고 P의 오버로딩된 연산자 ()를 호출했는데 이 함수가 두 인수의 합을 리턴하도록 되어 있으므로 결국 c에는 a+b인 3이 대입된다. 객체를 통해 멤버 함수를 호출했을 뿐 별로 희한할 것도 없는 예제이다. plus외에도 많은 함수 객체들이 미리 정의되어 있다.

 

함수 객체

연산

minus

 인수의 차를 계산한다.

multiplies

 인수의 곱을 계산한다.

divides

 인수를 나눈  몫을 리턴한다.

modulus

 인수를 나눈  나머지를 리턴한다.

negate

인수 하나를 전달받아 부호를 반대로 만든다.

equal_to

 인수가 같은지 비교하여 결과를 bool타입으로 리턴한다.

not_equal_to

 인수가 다른지 비교한다.

greater

 번째 인수가  번째 인수보다 큰지 조사한다.

less

 번째 인수가  번째 인수보다 작은지 조사한다.

greater_equal

 번째 인수가  번째 인수보다 크거나 같은지 조사한다.

less_equal

 번째 인수가  번째 인수보다 작거나 같은지 조사한다.

logical_and

 인수의 논리곱(&&) 결과를 리턴한다.

logical_or

 인수의 논리합(||) 결과를 리턴한다.

logical_not

인수 하나를 전달받아 논리부정(!) 리턴한다.

 

 

 2) 함수 객체의 활용

  => 기존의 sort의 올림차순이 아니라 함수 객체를 사용하여 다양한 옵션이 가능하다.

 

정렬을 위한 알고리즘 구현은sort가 하되 비교 방식만 함수 객체로 사용자가 지정할 수 있다.  좀 더 복잡한 객체 컨테이너라면 이차 정렬 조건을 둘 수 있는데 예를 들어 사원들을 이름순으로 정렬하 되 혹시 동명이인이 있으면 나이순으로 정렬하도록 세부 정렬 지침을 제공할 수 있다. 비교 구문이 인라 인으로 삽입되어 정렬 속도도 굉장히 빠른데 C의 qsort 함수보다도 훨씬 더 빠르다.

 sort 함수는 요소의 < 연산자로 대소를 비교하므로 기본적으로 올림차순으로 정렬하는데 함수 객체를 취하는 다음 버전을 사용하면 정렬 순서를 원하는대로 지정할 수 있다.

 

void sort(RanIt first, RanIt last, BinPred F);

 

마지막 인수 F는 비교할 두 요소를 전달받아 비교 결과를 리턴하는데 함수 객체의 조건을 만족하면 true를 리턴한다. bool형을 리턴하므로 F는 조건자 함수 객체이다. 다음 예제는 문자열을 정렬하는데 일반 sort 함수와 함수 객체 버전으로 각각 정렬한다.

 

  : sortdesc

#include <iostream>

#include <string>

#include <vector>

#include <algorithm>

#include <functional>

using namespace std;

 

void main()

{

     string names[]={"STL","MFC","owl","html","pascal","Ada",

          "Delphi","C/C++","Python","basic"};

     vector<string> vs(&names[0],&names[10]);

 

     //sort(vs.begin(),vs.end());

     sort(vs.begin(),vs.end(),greater<string>());

    

     vector<string>::iterator it;

     for (it=vs.begin();it!=vs.end();it++) {

          cout << *it << endl;

     }

}

 

sort의 기본 버전은 요소간의 비교를 위해 < 연산자, 즉 less 비교 함수 객체를 사용하도록 되어 있어 작은 값이 더 앞쪽에 온다. 그러나 greater 함수 객체를 사용하면 큰 값이 더 앞쪽에 오므로 정렬 순서는 반대가 된다.

 

만약 미리 제공되는 함수 객체가 아니라 사용자가 정의한 방식대로 정렬하고 싶다면 직접 함수 객체를 만들어 sort의 세 번째 인수로 전달한다. 다음 예제는 대소문자 구분없이 알파벳순으로 문자열을 오름차순 정렬한다.

 

  : sortfunctor

#include <iostream>

#include <string>

#include <vector>

#include <algorithm>

using namespace std;

 

struct compare {

     bool operator()(string a,string b) const {

          return stricmp(a.c_str(),b.c_str()) < 0;

     }

};

 

void main()

{

     string names[]={"STL","MFC","owl","html","pascal","Ada",

          "Delphi","C/C++","Python","basic"};

     vector<string> vs(&names[0],&names[10]);

 

     //sort(vs.begin(),vs.end());

     sort(vs.begin(),vs.end(),compare());

     vector<string>::iterator it;

     for (it=vs.begin();it!=vs.end();it++) {

          cout << *it << endl;

     }

}

 compare는 인수로 전달된 두 문자열 a, b를 대소문자 구분없이 비교하여 a가 더 작은지를 리턴한다.  compare를 쓰지 않는 sort는 string의 < 연산자로만 대소를 비교하므로 대문자가 항상 소문자 앞에 오 지만 compare를 사용하는 sort는 대소문자에 상관없이 알파벳순으로 정렬된다.

 

4. 함수 객체의 사용

 

  : dualinstance

#include <iostream>

#include <list>

#include <vector>

#include <algorithm>

using namespace std;

 

void functor1(int a)            // 함수 포인터

{

     printf("%d ",a);

};

 

struct functor2 {               // 함수 객체

     void operator()(double a) const {

          printf("%f\n",a);

     }

};

 

void main()

{

     int ari[]={1,2,3,4,5};

    vector<int> vi(&ari[0],&ari[5]);

     double ard[]={1.2,3.4,5.6,7.8,9,9};

     list<double> ld(&ard[0],&ard[5]);

 

     for_each(vi.begin(),vi.end(),functor1);

     cout << endl;

     for_each(ld.begin(),ld.end(),functor2());

}

 

main에서 벡터와 리스트 두 개의 컨테이너를 정의하고 for_each를 두 번 호출하여 두 컨테이너의 내용을 출력했다. 이때 각각 다른 함수 객체를 사용했는데 첫 번째 for_each는 함수 포인터를, 두 번째 for_each는 함수 객체를 사용했다. 이 둘은 원형도 다르고 값을 출력하는 방식도 다르다. 실행 결과는 다음과 같다.

 

1 2 3 4 5

1.200000

3.400000

5.600000

7.800000

9.000000

 

그렇다면 for_each 함수의 세 번째 인수는 도대체 어떤 타입이라고 설명할 수 있을까? 예제가 잘 동작하는 걸 보면 void (*)(int) 타입의 함수를 받기도 하고 void(*)(double) 타입의 () 연산자가 정의된 객체를 받기도 한다. 가변 인수도 아닌 함수가 두 개의 다른 타입을 어떻게 받아들일 수 있는가 말이다.

 

이 문제의 해답은 간단하다for_each는 함수가 아니라 함수를 만들 수 있는 템플릿일 뿐이며 호출부에서 전달되는 타입에 맞게 매번 구체화된다. 어떤 타입을 정해 놓고 받는게 아니라 들어오는대로 받아들여 구체화되는 것이다. 물론 전달된 타입은 템플릿 본체의 코드를 100% 지원하는 타입이어야 한다. 위 예에서 for_each 함수의 실체는 두 개 존재하며 각 버전이 받아들이는 타입이 다르다.

 

STL은 알고리즘이 어떤 함수를 호출할 것인지에 대한 모든 결정을 컴파일시에 수행한다. 조건만 맞다면 그게 함수건 객체건 가리지 않으며 그래서 일반적이라고 하는 것이다. 컴파일 타임에 모든 점검과 결정이 이루어지므로 컴파일 시간은 조금 더 걸리겠지만 실행시의 효율은 좋을 수밖에 없다

 

5. 함수 객체의 분류

 

함수 객체가 하는 일은 비교, 대입, 합산 등 알고리즘 구현중에 필요한 연산을 처리하는 것이라고 할 수 있다. 취하는 피연산자 개수로 연산자를 분류하듯이함수 객체도 필요한 인수의 개수로 분류할 수 있으며 리턴값의 타입도 중요한 분류 기준이다. STL은 인수와 리턴값, 즉 원형에 따라 함수 객체를 다음과 같이 분류하고 고유의 이름을 부여한다.

 

인수의 개수

bool 아닌 리턴값

bool 리턴

없음

Gen

 

단항

UniOp

UniPred

이항

BinOp

BinPred

 

UniOp는 인수 하나를 취하는 단항 함수 객체이며 BinPred는 인수 둘을 취해 bool형을 리턴하는 조건자 함수 객체이다. 피연산자를 하나도 취하지 않는 함수 객체를 생성기(Generator)라고 하는데 입력없이 혼자 무엇인가를 만들어 내는 역할만 한다. 대표적으로 난수를 생성하는 함수 객체가 생성기이다. 함수 객체를 칭하는 이 표기만 보면 필요한 함수의 원형을 쉽게 유추할 수 있다.

 

알고리즘 함수들은 예외없이 템플릿 함수로 구현되어 있는데 함수 객체에 해당하는 템플릿 인수의 이름에 어떤 종류의 함수 객체가 요구되는지 표기된다. 마치 함수의 형식 인수 이름에 의미있는 이름을 붙여 유용한 정보를 표기하는 것과 같다. 앞에서 배운 몇 개의 알고리즘 함수 원형을 살펴보면 마지막 인수인 함수 객체에 이러한 정보가 포함되어 있다.

 

InIt find_if(InIt first, InIt last, UniPred F);                 // 단항 리턴

void sort(RanIt first, RanIt last,BinPred F);           // 이항 리턴  

T accumulate(InIt first, InIt last, T val,BinOp op);   // 더하므로 두개의 인자

 

find_if의 세 번째 인수는 UniPred로 되어 있으므로 인수 하나를 취하고 bool형을 리턴하는 단항 조건자임을 쉽게 알 수 있다. find_if와 함께 사용할 수 있는 함수 또는 함수 객체의 () 연산자 원형은 다음과 같을 것이다.

 

bool Pred(T &val) { }

 

여기서 T는 물론 검색 대상 컨테이너의 요소 타입이며 함수 호출문의 실인수 타입으로 구체화된다. 검색 대상인 val 인수는 값으로 받든 레퍼런스로 받든 함수 본체에서 val을 참조하는 구문에는 영향을 주지 않으므로 아무래도 상관없다. sort 함수는 두 개의 인수를 전달받아 두 인수를 비교한 후 bool형을 리턴하는 함수 객체를 요구하며 accumulate의 함수 객체는 두 인수를 전달받아 모종의 연산을 한다는 것을 알 수 있다.

만약 알고리즘 함수가 요구하는 원형과 다른 함수 객체를 인수로 전달하면 어떻게 될까? for_each 함수를 테스트하는 functor 예제의 print 함수 객체를 다음과 같이 수정해 보자

 

 

. for_each는 단항 함수 객체(UniOp)를 요구하는데 에러를 유발시키기 위해 일부러 두 개의 인수를 받도록 했다.

 

struct print {

     void operator()(int a, int b) const {

          printf("%d\n",a);

     }

};

 

문법상의 문제는 없으므로 이 객체 정의문 자체는 에러가 아니다. 그러나 이 객체를 사용하는 곳에서 문제가 발생하는데 for_each의 본체에서, 즉 algorithm 헤더 파일에서 에러가 발생한다. for_each는 아마도 다음과 같이 구현되어 있을 것이다.

 

UniOp for_each(InIt first, InIt last, UniOp op)

{

     for (;first != last; ++first)

          op(*first);                    // 여기서 에러 발생

     return (op);

}

 

for_each는 구간을 순회하면서 매 요소마다 op 함수 객체를 호출하는데 인수는 현재 순회중인 반복자의 값 *first 하나밖에 없다. 하지만 이 값을 전달받는 객체의 () 연산자 함수와는 원형이 맞지 않으므로 호출할 수 없다는 컴파일 에러가 발생하는 것이다. 정확하게는 템플릿 함수가 구체화되는 과정의 템플릿 본체에서 구문 에러가 발생한다.

런타임 중에 발생하는 것이 아니라 컴파일중에 뭔가 잘못되었다는 것을 즉시 알 수 있으므로 위험하지는 않다. 이런 특성을 타입에 대한 안정성이라고 하는데 오동작할 소지가 있는 코드를 컴파일중에 명백한 에러로 처리하여 실생시의 버그를 최소화한다. 이번에는 다음과 같이 리턴값의 타입만 다르게 수정해 보자.

 

struct print {

     int operator()(int a) const {

          return printf("%d\n",a);

     }

};

 

for_each는 함수 객체를 호출하기만 할 뿐 리턴값을 요구하지는 않는다. 하지만 이렇게 수정해도 별 문제는 없다. 리턴값을 넘기더라도 for_each에서 이 값을 무시할 수 있고 for_each 템플릿의 본체와 충돌하는 부분이 없기 때문이다. 만약 템플릿 본체에서 리턴값을 명시적으로 요구할 때는 리턴값 타입도 항상 정확해야 한다. sortfunctor 예제의 compare 함수 객체를 다음과 같이 수정해 보자.

 

struct compare {

     void operator()(string a,string b) const {

          stricmp(a.c_str(),b.c_str()) < 0;

     }

};

 

이 함수 객체는 두 개의 정렬 대상을 전달받아 앞 뒤를 가려 주는 역할을 하므로 비교 결과를 반드시 리턴해야 하는데 void형으로 잘못 작성했다. 이렇게 되면 sort 템플릿 본체에서 비교 결과를 사용하는 부분에서 에러가 발생한다. sort의 내부에는 아마 다음과 같은 코드가 작성되어 있을 것이다. 물론 실제 코드는 컴파일러마다 다르다.

 

if (op(*first, *(first-1))

 

op 함수 객체로 두 요소를 넘겨 비교하도록 하고 그 결과에 따라 요소를 재배치해야 하는데 op의 결과가 없으므로 if문에 사용할 수 없는 것이다. compare 객체의 () 연산자가 int를 리턴하도록 수정하는 것은 가능하다. int는 bool형과 호환 타입이고 if문의 조건절로 사용될 수 있기 때문이다.

 

어떤 건 되고 어떤 건 안되고 함수 객체의 올바른 형태를 결정하는 것이 굉장히 어려운 규칙인 것 같지만 원칙은 지극히 간단하다.

 

템플릿의 타입은 본체의 모든 조건을 만족해야 한다는 동일한 알고리즘 조건이라는 것이 있는데 바로 이 원칙에만 맞게 작성하면 된다. for_each의 본체에 맞는 함수 객체이기만 하면 되고 sort가 구현하는 코드를 제대로 실행할 수 있으면 되는 것이다. 알고리즘의 목적과 동작 과정을 잘 생각해 보면 아주 상식적이다. 비교 함수는 bool을 리턴하는게 당연하고 for_each의 인수는 하나일 수밖에 없다.


Posted by JJOREG