제 19 장: STL 총칭 알고리즘

총칭 알고리즘을 사용하기에 앞서 <algorithm> 헤더를 포함해야 한다. (아래에 정의된) Operators 범주는 예외이다. Operators 범주에서 총칭 알고리즘을 사용하려면 <numeric> 헤더를 포함해야 한다.

이전 장에 표준 템플릿 라이브러리(STL)를 소개했다. STL의 중요한 요소인 총칭 알고리즘은 다루지 않았는데 범위가 아주 넓기 때문이다. 시간이 지나면서 STL은 크게 성장했다. 주로 템플릿의 중요성이 높아진 덕분이다. STL 장에 총칭 알고리즘을 다루면 좀 이상할 것 같았다. 그래서 총칭 알고리즘에 따로 별도의 장을 할애했다.

19.1: 총칭 알고리즘

총칭 알고리즘은 놀라운 작업을 수행한다. 템플릿의 힘 덕분에 알고리즘은 유형에 안전함을 유지하면서 다양한 유형에 광범위하게 적용될 정도로 개발되었다. 그 대표적인 예가 sort 총칭 알고리즘이다. 대조적으로 C라면 프로그래머가 직접 역호출 함수를 작성해야 하며 이 함수는 유형에 안전하지 않은 void const * 매개변수를 사용하지 않을 수 없다. 때문에 내부적으로 프로그래머는 유형 변환에 의존하지 않을 수 없는데 반하여, STL의 sort에서는 그저 다음과 비슷하게 서술하기만 하면 된다.
    sort(first-element, last-element)

가능하면 총칭 알고리즘을 사용해야 한다. 자주 마주하는 알고리즘을 위하여 따로 코드를 설계하고 싶은 욕망은 버리는게 좋다. 습관처럼 먼저 총칭 알고리즘 중에서 적절한 후보를 골라야 한다. 총칭 알고리즘은 코드를 작성할 때 여러분이 고른 무기가 되어야 한다. 완전히 사용법을 숙지하고 `습관'처럼 익히시기를 바란다.

이 장은 STL의 총칭 알고리즘을 알파벳 순으로 나누어서 연구한다. 각 알고리즘에 대하여 다음 정보를 제공한다.

알고리즘의 원형에서 Type총칭 데이터 유형을 가리킨다. 또, 특별한 유형의 반복자는 물론이고 (예를 들어 plus<Type> 같이 이항 연산을 수행하는) 다른 총칭 유형도 필요하면 언급한다 (18.2절). 반복자는 추상 컨테이너와 거의 미리-정의된 데이터 구조로 제공되지만 어느 시점이 되면 자신만의 반복자를 설계하고 싶을 것이다. 22.14절에 자신만의 반복자 클래스를 구성하는데 필요한 지침을 제공하고 다양한 유형의 반복자를 구현해야 하는 연산자들을 소개한다.

거의 모든 총칭 알고리즘은 반복자 [first, last) 범위를 요구하는데, 이 범위의 원소에 알고리즘이 적용된다. 반복자는 객체나 값을 가리킨다. 반복자가 Type 값이나 객체를 가리킬 때 알고리즘이 사용하는 함수객체는 Type const & 객체나 값을 받는다. 함수객체는 인자로 받은 객체를 변경할 수 없다. 이 사실은 총칭 알고리즘을 변형하기에는 적용되지 않는다. 물론 알고리즘이 적용되는 객체는 변경할 수 있다.

총칭 알고리즘을 범주별로 나눌 수 있다. 이 책에서는 다음 범주와 같이 총칭 알고리즘을 분류한다.

19.1.1: accumulate

19.1.2: adjacent_difference

19.1.3: adjacent_find

19.1.4: binary_search

19.1.5: copy

19.1.6: copy_backward

19.1.7: count

19.1.8: count_if

19.1.9: equal

19.1.10: equal_range

19.1.10.1: exchange

19.1.11: fill

19.1.12: fill_n

19.1.13: find

19.1.14: find_end

19.1.15: find_first_of

19.1.16: find_if

19.1.17: for_each

예제는 또한 for_each 알고리즘을 const와 비-const 매개변수를 정의한 함수와 사용할 수도 있다는 것을 보여준다. 또, for_eachtransform 총칭 알고리즘 사이의 차이점은 19.1.63항을 보라.

for_each 알고리즘은 자신의 객체를 변경하기 위하여 멤버 함수 안에 직접적으로 (즉, *this를 함수객체의 인자로 건네서) 사용할 수 없다. for_each 알고리즘은 먼저 건네어진 함수객체의 사본을 만들기 때문이다. 람다 함수 또는 포장 클래스(wrapper class)가 이 문제를 해결해 준다. 생성자가 현재 객체와 그의 멤버 함수중 하나를 가리키는 포인터나 참조를 받으면 된다.

19.1.18: generate

19.1.19: generate_n

19.1.20: includes

19.1.21: inner_product

19.1.22: inplace_merge

19.1.23: iter_swap

19.1.24: lexicographical_compare

19.1.25: lower_bound

19.1.26: max

19.1.27: max_element

19.1.28: merge

19.1.29: min

19.1.30: min_element

19.1.31: mismatch

19.1.32: next_permutation

19.1.33: nth_element

19.1.34: partial_sort

19.1.35: partial_sort_copy

19.1.36: partial_sum

19.1.37: partition

19.1.38: prev_permutation

19.1.39: random_shuffle

19.1.40: remove

19.1.41: remove_copy

19.1.42: remove_copy_if

19.1.43: remove_if

19.1.44: replace

19.1.45: replace_copy

19.1.46: replace_copy_if

19.1.47: replace_if

19.1.48: reverse

19.1.49: reverse_copy

19.1.50: rotate

19.1.51: rotate_copy

19.1.52: search

19.1.53: search_n

19.1.54: set_difference

19.1.55: set_intersection

19.1.56: set_symmetric_difference

19.1.57: set_union

19.1.58: sort

19.1.59: stable_partition

19.1.60: stable_sort

예제는 자주 일어나는 문제에 대한 해결책을 구현하고 있음을 눈여겨보라: 여러 계통적 기준을 사용하여 어떻게 정렬할 것인가. 예제에서 다음 몇 가지를 더 주목할 가치가 있다.

19.1.61: swap

19.1.62: swap_ranges

19.1.63: transform

for_each (19.1.17항)와 transform 총칭 알고리즘 사이의 다음 차이를 꼭 알아야 한다. transform 총칭 알고리즘 대신에 범위-기반의 for 회돌이를 사용할 수 있음도 주목하자. 그렇지만 범위-기반의 for-회돌이와 다르게 transform 알고리즘은 부-범위와 역방향 반복자와도 사용될 수 있다.

19.1.64: unique

19.1.65: unique_copy

19.1.66: upper_bound

19.1.67: 힙(Heap) 알고리즘

힙은 배열로 표현이 가능한 일종의 이진 트리이다. 표준 힙에서 한 원소의 키는 자손의 키보다 작지 않다. 이런 유형의 힙을 max heap이라고 부른다. 숫자가 키인 트리는 다음 그림 21과 같이 조직할 수 있다.

Figure 21 is shown here.
그림 21: 힙의 이진 트리 표현

이런 트리는 배열로 조직할 수도 있다.
        12, 11, 10, 8, 9, 7, 6, 1, 2, 4, 3, 5
다음 설명에서 이 배열에 있는 두 개의 포인터를 눈여겨보라: node 포인터는 트리의 다음 노드 위치를 나타낸다. child 포인터는 node 포인터의 자손인 다음 원소를 가리킨다. 처음에 node는 첫 원소를 가리킨다. 그리고 child는 두 번째 원소를 가리킨다. child는 이제 배열을 넘어서 가리키기 때문에 나머지 노드는 자손이 없다. 그래서 노드 6, 1, 2, 4, 3 그리고 5는 자손이 없다.

왼쪽 오른쪽 가지에 순서가 없는 것을 눈여겨보라. 8은 9보다 작지만 7은 6보다 크다.

최상단 노드부터 시작하여 이진 트리 레벨 순서로 순회하며 힙이 생성된다. 최상단 노드는 12이고 레벨은 0이다. 첫 레벨에 11과 10이 있다. 두 번째 레벨에 8, 9, 7 그리고 6이 있다 등등.

힙은 무작위 접근을 지원하는 컨테이너 안에 구성할 수 있다. 그래서 리스트는 힙에 적절한 데이터 구조가 아니다. 힙은 (정렬되지 않은) 배열로부터 구성할 수 있다 (make_heap 사용). 최상위-원소를 힙으로부터 제거하고 (pop_heap 사용) 새로운 원소를 힙에 추가하면 (push_heap 사용) 원소들이 재배열된다. 그리고 힙에 있는 원소들을 정렬할 수 있다 (sort_heap를 사용하면 된다. 물론 이렇게 하면 힙은 무효화 된다).

19.1.67.1: `make_heap' 함수

19.1.67.2: `pop_heap' 함수

19.1.67.3: `push_heap' 함수

19.1.67.4: `sort_heap' 함수

19.1.67.5: 힙 함수를 사용한 예제

다음 예제는 힙을 조작하는 다양한 총칭 알고리즘을 보여준다.
    #include <algorithm>
    #include <iostream>
    #include <functional>
    #include <iterator>
    using namespace std;

    void show(int *ia, char const *header)
    {
        cout << header << ":\n";
        copy(ia, ia + 20, ostream_iterator<int>(cout, " "));
        cout << '\n';
    }
    int main()
    {
        int ia[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
                    11, 12, 13, 14, 15, 16, 17, 18, 19, 20};

        make_heap(ia, ia + 20);
        show(ia, "The values 1-20 in a max-heap");

        pop_heap(ia, ia + 20);
        show(ia, "Removing the first element (now at the end)");

        push_heap(ia, ia + 20);
        show(ia, "Adding 20 (at the end) to the heap again");

        sort_heap(ia, ia + 20);
        show(ia, "Sorting the elements in the heap");


        make_heap(ia, ia + 20, greater<int>());
        show(ia, "The values 1-20 in a heap, using > (and beyond too)");

        pop_heap(ia, ia + 20, greater<int>());
        show(ia, "Removing the first element (now at the end)");

        push_heap(ia, ia + 20, greater<int>());
        show(ia, "Re-adding the removed element");

        sort_heap(ia, ia + 20, greater<int>());
        show(ia, "Sorting the elements in the heap");
    }
    /*
        출력:
            The values 1-20 in a max-heap:
            20 19 15 18 11 13 14 17 9 10 2 12 6 3 7 16 8 4 1 5
            Removing the first element (now at the end):
            19 18 15 17 11 13 14 16 9 10 2 12 6 3 7 5 8 4 1 20
            Adding 20 (at the end) to the heap again:
            20 19 15 17 18 13 14 16 9 11 2 12 6 3 7 5 8 4 1 10
            Sorting the elements in the heap:
            1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
            The values 1-20 in a heap, using > (and beyond too):
            1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
            Removing the first element (now at the end):
            2 4 3 8 5 6 7 16 9 10 11 12 13 14 15 20 17 18 19 1
            Re-adding the removed element:
            1 2 3 8 4 6 7 16 9 5 11 12 13 14 15 20 17 18 19 10
            Sorting the elements in the heap:
            20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1
    */

19.2: STL: 함수 어댑터 심화 연구

함수 어댑터를 사용하기 전에 <functional> 헤더를 포함해야 한다.

멤버 함수 어댑터는 표준 템플릿 라이브러리의 일부이다. 총칭 알고리즘과 조합해 사용하면 아주 유용하다. 이 때문에 STL에 할애한 이전 장보다 이 장에 다룬다.

STL에 정의된 멤버 함수 어댑터로 (매개변수 없는) 클래스 객체의 멤버 함수를 마치 비-멤버 함수처럼 호출할 수 있다. 그리고 이항 또는 단일 인자의 함수를 함수객체 안으로 합성해 넣으면 총칭 알고리즘에 결합해 사용할 수 있다.

다음 절에 멤버 함수를 비-멤버 함수처럼 호출하는 방법을 다룬다. 그 다음에 어댑터를 적용할 수 있는 함수를 다룬다.

19.2.1: 멤버 함수 어댑터

이 절에 소개하는 멤버 함수 어댑터를 사용하면 호출할 함수가 클래스의 멤버 함수일 때 총칭 알고리즘을 사용할 수 있다. 그렇지만 미래의 C++17 표준에서 비추천되었다. bind로 쉽게 대체할 수 있기 때문이다. 멤버 함수 어댑터를 간략하게 언급한다. 대신에 bind를 어떻게 사용할 수 있는지 보여준다.

다음 클래스를 연구해 보자:

    class Data
    {
        public:
            void display();
    };
확실히 display 멤버는 Data 객체에 저장된 정보를 보여준다.

Data 객체를 벡터에 저장할 때 for_each 총칭 알고리즘은 벡터에 저장된 각 객체의 display 멤버를 호출하는 데 사용하기가 쉽지 않다. for_each가 (비-멤버) 함수나 함수객체는 세 번째 인자로 받지만 멤버 함수는 받지 않기 때문이다.

멤버 함수 어댑터 mem_fun_refbind를 사용하면 이 문제를 해결할 수 있다. mem_fun_ref는 매개변수가 정의되어 있지 않은 멤버 함수의 주소를 예상한다. 그리고 mem_fun_ref의 함수 호출 연산자는 자신에게 인자로 건네어진 객체에 대하여 그 멤버 함수를 호출한다. 예제는 그 사용법과 더불어 bind의 또다른 사용법을 보여준다. (std 이름공간과 placeholders 이름공간을 사용한다고 간주한다):

    int main()
    {
        vector<Data> data;
                                            // C++17에서 비추천됨
        for_each(data.begin(), data.end(), mem_fun_ref(&Data::display));

                                            // 다른 방식, bind 사용
        for_each(data.begin(), data.end(), bind(&Data::display, _1));
    }

두 번째 멤버 함수 어댑터는 mem_fun이다. 객체를 가리키는 포인터로부터 멤버 함수를 호출하는 데 사용된다. 여기에서 bind도 사용할 수 있다. data 벡터에 있는 포인터를 (사용된다면 shared_ptr 객체도 역시) 탐지하기 때문이다.

    int main()
    {
        vector<Data *> data { new Data };

                                            // C++17에서 비추천됨
        for_each(data.begin(), data.end(), mem_fun(&Data::display));

                                            // 다른 방식, bind 사용
        for_each(data.begin(), data.end(), bind(&Data::display, _1));

        delete data[0];
    }

19.2.2: 적용가능 함수

STL의 문맥에서 적용가능 함수란 다음을 정의하고 있는 함수객체이다. 그리고 함수 호출 연산자의 반환 값 유형으로서 result_type을 정의하고 있는 함수객체이다.

STL은 pointer_to_unary_functionpointer_to_binary_function을 어댑터로 정의하고 있다. 각각 단항 함수와 이항 함수를 포인터로 받아서 적용가능한 함수로 변환한다.

ptr_fun에 단일 함수를 건네면 pointer_to_unary_function를 사용하여 적용가능한 단일 함수를 만든다. 이항 함수를 제공하면 pointer_to_binary_function를 사용하여 적용가능한 이항 함수를 만든다.

그렇지만 이 어댑터들은 ptr_fun과 함께 미래의 C++17 표준에서는 비추천되었다. 왜냐하면 쉽게 람다 표현식으로 교체할 수 있기 때문이다.

다음 예제는 ptr_fun의 사용법을 보여준다. 적용가능한 이항 함수를 만든다. main 함수는 검색할 단어를 표준 입력 스트림으로부터 또 string 객체에 저장된 추가 단어들로부터 추출한다. 목표 단어가 발견되면 string 객체의 벡터에서 목표 단어 다음에 있는 단어를 보여준다. 검색은 대소문자를 구별하지 않고 POSIX 함수인 strcasecmp를 사용한다. ptr_fun을 사용한 다음에 동등한 표현식을 람다 표현식을 사용하여 보여준다.

#include <vector>
#include <algorithm>
#include <functional>
#include <cstring>
#include <string>
#include <iterator>
#include <iostream>

using namespace std;

inline int stringcasecmp(string lhs, string rhs)
{
    return strcasecmp(lhs.c_str(), rhs.c_str());
}

int main()
{
    string target;
    cin >> target;

    vector<string> v1;
    copy(istream_iterator<string>(cin), istream_iterator<string>(),
        back_inserter(v1));

    auto pos = find_if(                         // C++17에서 비추천
                    v1.begin(), v1.end(),
                    not1( bind2nd(ptr_fun(stringcasecmp), target) )
               );

                                                // 더 좋은 대안은
    auto pos = find_if(v1.begin(), v1.end(),    // 람다 표현식이다.
            [&](auto const &str)
            {
                return not stringcasecmp(str, target);

            }
        );

    if ( pos != v1.end())
       cout <<   "The search for `" << target << "' was successful.\n"
                 "The next string is: `" << pos[1] << "'.\n";
}

    // 입력:
    //  VERY   I have existed for years, but very little has changed
    // 프로그램의 출력:
    //  The search for `VERY' was successful.
    //  The next string is: `little'.

민감한 독자라면 별로 효율적인 프로그램이 아님을 눈치채셨을 것이다. stringcasecmp 함수는 값 유형의 매개변수를 정의한다. 때문에 stringcasecmp가 호출될 때마다 인자의 사본을 생성해 거기에 건네야 한다. 그러나 매개변수 정의를 다음과 같이 바꾸면

    inline int stringcasecmp(string const &lhs, string const &rhs)
컴파일러는 다음과 같은 에러 메시지를 토해 낸다.
In instantiation of 'std::binder2nd<std::pointer_to_binary_function<
        const std::string&, const std::string&, int> >':

    typename _Operation::result_type std::binder2nd<_Operation>::operator()(
            typename _Operation::first_argument_type&) const
cannot be overloaded with:
    typename _Operation::result_type std::binder2nd<_Operation>::operator()(
            const typename _Operation::first_argument_type&) const
그렇지만 두 번째 find_ifstringcasecmp의 값 매개변수를 const & 매개변수로 바꾸면 문제없이 컴파일된다. 이 때문에 예제에 람다 표현식을 선호한 것이다.