본문 바로가기

Software/C/C++

[C++.STL]3장: 함수(function)와 조건자(predicate)

3.1 함수(function)

표준 라이브러리에서 제공하는 알고리듬중에는 함수를 인자로 요구하는 것들이 많다. 예를 들어, for_each() 알고리듬은 컨테이너에 담긴 각각의 원소에 인자로 넘겨받은 함수를 적용한다. 아래는 printElement() 함수를 사용하여 정수 list의 원소들을 출력하는 코드이다.
    void printElement(int value)
    {
       cout << "The list contains " << value << endl;
    }
    
    int main() 
    {
       list<int> aList;
          ...
       for_each(aList.begin(), aList.end(), printElement);
    }
이항 함수(binary function)는 두개의 인자를 취하는데, 서로 다른 두 시퀀스에서 값들을 하나씩 가져와 함수에 넘겨준다. 예를 들어, 문자열 list와 정수 list가 주어지고, 문자열 list에 담긴 각각의 문자열을 정수 list에 담긴 숫자만큼 복사한다고 할 때, 표준 라이브러리에서 제공하는 transform() 함수를 사용하면 쉽게 할 수 있다. 이를 위해 우선 원하는 기능을 가진 이항 함수를 정의한다.
    // 'base'를 'number' 갯수만큼 복사한다. 
    string stringRepeat(const string& base, int number)
    {
       string result;   // 'result'는 처음에 비어 있다.
       while (number--)  result += base;
       return result;
    }
그리고, 다음과 같이 transform()을 호출하면 원하는 결과를 얻을 수 있다.
    list<string> words;
    list<int> counts;
       ...
    transform(words.begin(), words.end(), counts.begin(),
       words.begin(), stringRepeat);
문자열 list (one, two, three)과 정수 list (3, 2, 3)를 인자로 하여transform()을 호출하면, (oneoneone, twotwo, threethreethree)의 결과를 얻을 수 있다.
 
 
 

3.2 조건자(predicate)

조건자(predicate)는 불값(true/false) 또는 정수값을 리턴하는 함수이다. 기존 C에서의 관행에 따라, 영이 아닌 정수값은 참으로 간주하고, 영인 경우에는 거짓으로 간주한다. 다음은 정수값을 인자로 받아 윤년이면 참을, 아니면 거짓을 리턴하는 함수이다.(아래 윤년 프로그램은 안재형님이 제공해 주셨습니다.)
    // 'year'가 윤년에 해당하면 참을 리턴한다.
    bool isLeapYear(unsigned int year)
    {
        bool flag = false;
    
        if ((year % 4) == 0) {              // 4년마다 윤년이고,
            flag = true;
            if ((year % 100) == 0) {         // 그중 100년마다 윤년이 아니고,
                flag = false;
                if ((year % 400) == 0) {     // 그중 400년마다 윤년
                    flag = true;
                }
            }
        }
    
        return flag;
    }
generic 알고리듬의 인자로 조건자를 사용할 수 있는데, find_if() 알고리듬을 예로 들어보자. 이 함수는 조건자를 만족하는 첫번째 원소를 가리키는 반복자를 리턴하는데, 만약 이 조건자를 만족하는 원소가 없으면 end-of-range 값을 리턴한다. 이 알고리듬을 사용하여, 다음과 같이 년도들로 구성된 list(aList)에서 첫번째로 발견된 윤년의 위치를 리턴하는 코드를 만들 수 있다.
    list<int>::iterator firstLeap = 
       find_if(aList.begin(), aList.end(), isLeapYear);
 
 

3.3 함수 객체(function object)

 
함수 객체(function object)는 괄호 연산자를 멤버함수로 가지는 클래스의 객체이다. 함수 대신에 함수 객체를 사용하는 것이 더 편리한 경우가 많은데, 함수 객체를 함수로 사용하면, 함수가 호출될 때마다 함수 객체의 괄호 연산자가 호출된다. 예를 들어, 다음 클래스 정의를 살펴보자.
    class biggerThanThree {
    public:
       bool operator()(int val) { return val > 3; }
    };
biggerThanThree 클래스의 객체를 함수 호출 형태를 사용하여 참조하게 되면, 멤버함수로 정의된 괄호 연산자가 호출된다. 이번에는 이 클래스를 일반적인 용도에 쓸 수 있도록 다듬어 보자. 생성자와 데이터 멤버를 추가한다.
    class biggerThan {
    public:
       const int testValue;
       biggerThan(int x) : testValue(x) { }
    
       bool operator()(int val) { return val > testValue; }
    };
이렇게 함으로써 'X보다 큰가?'에 해당하는 함수 객체를 만들 수 있고, 여기서 X의 값은 함수 객체를 생성할 때 정할 수 있다. 조건자(predicate)를 인자로 요구하는 generic 함수에 함수 객체를 인자로 넘길 수 있는데, 다음은 list에서 12보다 큰 첫번째 값의 위치를 찾아내는 코드이다.
    list<int>::iterator firstBig =
       find_if(aList.begin(), aList.end(), biggerThan(12));
일반 함수 대신에 함수 객체를 사용하는 가장 큰 이유로 세가지를 들 수 있는데, 첫째로, 새로운 함수를 만들지 않고 표준 라이브러리에서 제공되는 함수 객체를 사용하자는 것이고, 둘째로, 인라인 함수를 호출함으로써 수행속도를 향상하고자 하는 것이고, 셋째로, 함수 객체로 하여금 자신이 가지고 있는 상태정보를 접근하고, 갱신할 수 있도록 하자는 것이다. 프로그래머 입장에서는 세번째가 가장 매력적인 이유가 아닐까 싶다. 그럼, 각각의 예를 들어보자.

A. 재사용성

다음 표는 표준 라이브러리에서 제공되는 함수 객체를 설명하고 있다. 이들 함수 객체를 사용하기 위해서는, #include <functional>을 통하여 반드시 헤더 화일을 포함시키도록 한다.
 
이름
연산
산술 함수(arithmetic function)
plus 
minus 
multiplies 
divides 
modulus 
negate
x + y
x - y
x * y
x / y
x % y
-x
비교 함수(comparison function)
equal_to
not_equal_to
greater
less
greater_equal
less_equal
x == y 
x != y 
x > y 
x < y 
x >= y 
x <= y
논리 함수(logical function)
logical_and
logical_or
logical_not
x && y 
x || y 
!x
이것들이 어떻게 사용되는지 몇가지 예를 들어 살펴보자. 첫번째 예는 plus()를 사용하여 정수들로 구성된 두개의 list(listOne, listTwo)를 원소별로 더하여, 그 결과를 첫번째 list(listOne)에 배치한다.
    // 이항 연산자인 경우: transform()의 네번째 인자에 연산의 결과값이 들어감
    transform(listOne.begin(), listOne.end(), listTwo.begin(), 
       listOne.begin(), plus<int>());
두번째 예는 logical_not()을 사용하여 불값으로 이루어진 vector내의 모든 원소를 부정하는 예이다.
    // 단항 연산자인 경우: transform()의 세번째 인자에 연산의 결과값이 들어감
    transform(aVec.begin(), aVec.end(), aVec.begin(),
       logical_not<bool>());
사용자 삽입 이미지
클래스 정의의 위치 
표준 라이브러리가 위 표에서 보여준 함수들을 정의할 때 사용하는 베이스 클래스들은 새로운 함수 객체들(단항, 이항)을 만들 때 필요하다. 이들 베이스 클래스들은 다음과 같다.
    template <class Arg, class Result>
    struct unary_function {
       typedef Arg argument_type;
       typedef Result result_type;
    };
    
    template <class Arg1, class Arg2, class Result>
    struct binary_function {
       typedef Arg1 first_argument_type;
       typedef Arg2 second_argument_type;
       typedef Result result_type;
    };
이들 함수들을 사용하는 예는 6.3절에서 살펴본다. 여기서는 "Widget" 타입과 정수 타입을 인자로 받고, widget의 아이디 번호와 정수값을 서로 비교하는 이항 함수를 정의해보자.
    struct WidgetTester : binary_function<Widget, int, bool> {
    public:
       bool operator()(const Widget& wid, int testid) const
          { return wid.id == testid; }
    };

B. 성능

함수 대신에 함수 객체를 사용하는 두번째 이유로 수행속도의 향상을 들었는데, 많은 경우에 위에서 예로든 transform()처럼 함수 객체의 호출을 in-line으로 대치함으로써 함수 호출의 오버헤드를 없앨 수 있게 된다. 
사용자 삽입 이미지
 함수 객체를 사용하여 레퍼런스 저장하기 

C. 유연성("smart function")

함수대신에 함수 객체를 사용하는 세번째 이유는 함수를 매번 호출할 때마다 이전 호출시의 상태를 기억해야 될 상황이 있기 때문이다. 이러한 예로, generate() 알고리듬에 사용되는 발생기(generator)를 생성할 때를 들수 있는데, 여기서 발생기란 매번 호출될 때마다 다른 값을 반환하는 함수를 말한다. 가장 많이 사용되는 형태의 발생기는 난수 발생기를 들 수 있겠지만, 이외에도 여러가지 용도로 쓰일 수 있다. 시퀀스 발생기는 단순히 1,2,3,4와 같이 자연수의 수열을 반환한다. 이 함수 객체를 APL에 있는 이와 비슷한 연산의 이름을 따서, iotaGen이라 하고, 다음과 같이 정의할 수 있겠다.
    class iotaGen {
    public:
       iotaGen(int start = 0) : current(start) { }
       int operator()() { return current++; }
    private:
       int current;
    };
iotaGen 객체는 생성자의 초기값이나 디폴트 값(0)을 유지하고 있다가, 함수호출 연산자가 호출될 때마다, 현재 값을 반환하고 자신의 값을 하나 증가시킨다. 이 객체를 사용하여 다음과 같이 표준 라이브러리 함수 generate()를 호출하면 20개의 원소를 가진 vector를 1부터 20까지의 값으로 초기화할 수 있다.
    vector<int> aVec(20);
    generate(aVec.begin(), aVec.end(), iotaGen(1));
 
 

3.4 함수 어댑터(function adaptor)

함수 어댑터(function adaptor)는 함수를 함수 객체로 사용할 수 있도록 전역함수 또는 멤버함수를 바꿔주는 클래스의 객체이다(함수 객체는 함수 또는 함수 객체의 행동을 바꾸는데 사용할 수도 있다. 이는 다음 절에서 다루도록 한다). 각각의 함수 어댑터는 전역 함수 또는 멤버 함수를 인자로 하는 생성자를 가지고 있다.
pointer_to_unary_functionpointer_to_binary_function 템플릿은 하나 또는 두개의 인자를 가지는 전역 함수를 개작하는데 사용된다. 이들 어댑터는 직접 적용할 수도 있고, ptr_fun 함수 템플릿을 사용하여 적절한 어댑터를 자동으로 생성할수도 있다. 예를 들어, 그냥 단순한 times3() 함수를 개작하여, 이를 정수 vector에 적용할 수 있다.
    int times3(int x)
    {
      return 3 * x;
    }
    
    int a{} {1,2,3,4,5};
    vector<int> v(a, a+5), v2;
    
    transform(v.begin(), v.end(), v2.end(), ptr_fun(times3));
또는, 어댑터를 적용하여 새롭게 개작된 함수 객체를 vector로 넘겨준다.
    pointer_to_unary_function<int, int> pf(times3);
    transform(v.begin(), v.end(), v2.end(), pf);
이렇게 하는 경우, 컴파일러가 ptr_fun을 사용하여 pointer_to_unary_function이 요구하는 타입을 추론할 수 있다는 장점이 있다. (?)
'mem_fun' 부류의 템플릿은 전역함수가 아닌 멤버함수를 개작하는데 사용된다. 예를 들어, list의 set을 가지고 있고, set내의 각 list를 정렬하고 싶다면, mem_fun_t나 mem_fun을 사용하여 set내의 각 list에 list 정렬함수를 적용할 수 있다.
    set<list<int>* > s;
    
    // set 원소들을 list들로 초기화한다
    ...
    // set에 속해있는 list들을 각각 정렬한다
    for_each(s.begin(),s.end(),mem_fun(&list<int>::sort));
    
    // 이제 set에 속한 각각의 list들이 모두 정렬되었다.
    
   This is necessary because the generic sort algorithm cannot be used on a list.
This is also the simplest way to access any polymorphic characteristics of an object held in a standard container.
For instance I might invoke a virtual draw function on a collection of objects that are
all part of the canonical 'shape' hierarchy like this:
    // shape 상속구조
    class shape {
       virtual void draw();
    };
    
    class circle : public shape {
       void draw();
    };
    
    class square : public shape {
      void draw();
    };
    
    // vector에 shape들을 집어넣는다
    circle c;
    square s;
    vector<shape*> v;
    v.push_back(&s);
    v.push_back(&c);
    
    // vector에 담긴 각각의 shape에 대해 draw()를 호출한다
    for_each(v.begin(),v.end(), mem_fun(&shape::draw));
전역함수 어댑터와 비슷하게, 멤버함수 어댑터는 각각 하나의 클래스 템플릿과 이와 관련된 함수 템플릿으로 구성된다. 클래스가 실질적인 어댑터이며, 함수가 그 클래스의 인스턴스를 도중에 생성함으로써 클래스의 사용을 단순하게 만들어준다. (?)예를 들어, 위에서 mem_fun_t를 만들어서 for_each() 알고리듬으로 넘겨줄수도 있었다.
    mem_fun_t<shape> mf(&shape::draw);
    for_each(v.begin(), v.end(), mf);
이번에도, mem_fun 함수 템플릿이 컴파일러로 하여금 mem_fun_t가 필요로하는 타입을 알아낼 수 있도록 함으로써 mem_fun_t 어댑터의 사용을 단순화한 것을 볼 수 있다.(?)
라이브러리는 인자를 가지지 않는 함수와 1개의 인자를 가지는 함수들에 대해서 멤버 함수 어댑터를 제공한다. 하지만, 그 이상의 인자를 가지는 함수들에 대해서도 쉽게 확장이 가능하다.
 
 

3.5 부정자(negator)와 바인더(binder)

부정자(negator)와 바인더(binder)는 기존에 있던 함수 객체로부터 새로운 함수 객체를 만드는데 사용되는 함수 어댑터이다. 이들은 다른 함수나 generic 알고리듬을 호출하기에 앞서 인자 리스트를 구성하는 과정의 일부로 사용되는 것이 보통이다.
부정자 not1()not2()는 각각 단항과 이항 조건자 객체를 인자로 받아들여, 원래 값의 반대값을 내뱉는 새로운 함수 객체를 생성한다. 예를 들어, 앞절에서 정의한 WidgetTester 함수 객체를 이용하면, 함수객체
    not2(WidgetTester())
는 widget 검사기와 같은 인자를 가지고, widget 검사기가 참일때 거짓을, 거짓일때 참을 반환하는 이진 조건자를 만들어낸다. 부정자는 unary_functionbinary_function 클래스의 하위 클래스로 정의된 함수 객체하고만 사용된다.
바인더는 두개의 인자를 가지는 함수를 받아서, 첫번째 인자나 두번째 인자를 특정 값으로 바인드시켜 한개의 인자를 가지는 함수를 만들어 낸다. 이때, 바인더의 인자로 넘어오는 함수는 binary_function 클래스의 하위 클래스에 속해야 한다. bind1st() 바인더는 첫번째 인자를 바인드하고, bind2nd()는 두번째 인자를 바인드한다.
예를 들어, 바인더 bind2nd(greater<int>(), 5)는 5보다 큰지를 검사하는 함수객체를 만들어내며, 다음과 같이 쓰일 수 있다. 아래 예는 list에서 가장 먼저 발견되는 5보다 큰 수를 가리키는 반복자를 반환하는 코드이다.
    list<int>::iterator where = find_if(aList.begin(), aList.end(),
               bind2nd(greater<int>(), 5));
바인더와 부정자를 결합해서, 다음과 같은 함수객체를 생성할 수 있다. 이 함수객체는 인자가 3으로 나눠지면 참을, 그렇지 않으면 거짓을 반환한다. 아래 코드는 list로부터 3의 배수를 제거한다.
    list<int>::iterator where = remove_if(aList.begin(), aList.end(),
               not1(bind2nd(modulus<int>(), 3)));
아래에서 바인더는 이진 함수 WidgetTester() 함수를 호출할 때 widget 번호를 고정시켜서, widget만을 유일한 인자로 취하는 함수를 만들어낸다.
    list<Widget>::iterator wehave = 
       find_if(on_hand.begin(), on_hand.end(), 
          bind2nd(WidgetTester(), wid));