CodeSarang.Com
Home | All categories Join | Login | 검색   

 

STL 의 모든것

등록자 : rawmania (김정수), 2008-09-05
글수정 | 글삭제


  1. STL (Standard Template Library) (출처 : winapi.co.kr)
    1. 컨테이너(Container)
      1. 시퀀스 컨테이너
      2. 벡터(vector)
        1. 삽입과 삭제
        2. 연산자
        3. 사용자 정의 타입
        4. vector
      3. 리스트(List)
        1. 삽입과 삭제
        2. 리스트의 정렬
      4. 데크(Deque)
      5. 연관 컨테이너
        1. pair 구조체
      6. 셋(Set)
        1. 생성자
        2. 삽입과 삭제
        3. 객체의 셋
        4. 동등성과 동일성
        5. 집합 연산
      7. 맵(Map)
        1. 삽입과 삭제
        2. 검색
        3. 수정
      8. 컨테이너 어댑터
      9. 스택
      10. 큐(Queue)
      11. 우선순위 큐(Priority Queue)
    2. 반복자(Iterator)
      1. 반복자의 종류
        1. 입출력 반복자
        2. 순방향, 양방향 반복자
        3. 임의 접근 반복자
        4. 삽입 반복자
        5. 상수 반복자
        6. 역방향 반복자
        7. 반복자와 알고리즘
        8. 반복자와 속성
    3. 알고리즘(Algorithm)
    4. 함수객체(Functor)
      1. 함수객체의 사용
      2. 미리 정의된 함수 객체
      3. 함수객체의 종류
    5. 어댑터 (Adapter)
      1. 합수객체 어댑터
        1. 부정자
        2. 바인더
        3. 멤버함수 어댑터
    6. 할당기(Allocator)

STL (Standard Template Library) (출처 : winapi.co.kr)

http://www.50001.com/language/cppside/data/STL1.txt
http://idb.snu.ac.kr/~sjjung/stl/
http://winapi.co.kr/clec/cpp4/cpp4.htm

STL은 1979년경 알렉산더 스테파노프(Alexander Stepanov) 한 사람에 의해 창안되었다. 이 시기는 스트로스트룹이 C++의 초기 디자인을 한 시점과 거의 일치한다. C++과 STL은 최초 따로 탄생했고 각자의 길을 걸어가다가 90년대 중반에 비로소 하나로 통합되었다.
스테파노프는 최초 Ada로 STL의 원형을 작성했는데 Ada는 일반화 프로그래밍을 위한 적절한 특징들을 많이 가지고 있었다. 그러나 Ada 자체가 대중의 인기를 얻지 못하고 C++이라는 더 적합한 언어가 발표됨에 따라 C++로 개발 언어를 변경했다. 스테파노프가 STL을 발전시키는 동안 C++은 템플릿을 언어의 일부로 지원하기 시작했다. C의 강력한 포인터와 C++의 템플릿 기능은 STL을 구현하기 위한 최적의 환경이었다.
연구가 진행되면서 Dave Musser, Meng Lee 등의 공동 작업자들이 합류하여 STL을 더욱 정교하게 발전시켜 나갔으며 많은 사람이 STL로 프로젝트를 작성하기 시작했다. 결국 STL은 94년 7월 ANSI/ISO의 최종 승인을 거쳐 국제 표준이 되었으며 98년 C++ 표준인 14882의 일부로 채택됨으로써 C++ 라이브러리의 근간을 이루게 되었다. 표준 채택 이전에도 각 컴파일러 제작사들은 STL을 자사 라이브러리의 일부로 이미 제공하고 있었으며 실제 프로젝트에서도 활용되었다. 현재는 국제 표준으로 그 구조가 통일됨으로써 호환성과 이식성을 걱정할 필요없이 마음놓고 STL을 사용할 수 있다.

일반화(Generic)는 객체 지향의 다음 세대라고 일컬어지는데 두 가지 측면에서 일반성을 제공한다.
① 임의 타입에 사용할 수 있는 자료 구조를 만들 수 있다. 정수, 실수 등의 기본 타입은 물론이고 사용자 정의형 타입과 그 유도형까지도 관리할 수 있는 자료 구조를 정의할 수 있다. 자료 구조의 일반성을 구현하기 위해 인수로 전달된 타입으로 클래스를 정의하는 C++의 템플릿 문법이 사용된다.
② 자료 구조의 형태나 내부 구조에 상관없이 임의의 데이터 집합에 적용할 수 있는 일반화된 알고리즘을 제공한다. 자료 구조에 상관없이 사용 방법이 동일하므로 어떠한 형태의 데이터에 대해서도 적용할 수 있다. 논리적으로 비슷한 작업은 같은 방법으로 수행할 수 있으며 이를 위해 반복자라는 일반화된 포인터를 사용한다.

STL의 단점
① 템플릿에 기반하기 때문에 타입마다 함수와 클래스가 매번 구체화되어 코드가 비대해지는 고질적인 문제가 있다. 완전히 똑같은 컨테이너라도 타입이 바뀌면 두 벌의 거대한 코드 집합이 따로 생성된다.
② STL로 작성한 코드는 가독성이 심하게 떨어진다. 템플릿 자체가 익숙치 않은 문법인데다 템플릿 클래스의 타입명이 길어 얼른 의미를 파악하기 어렵다. 게다가 이중 삼중으로 템플릿이 중첩되면 만든 사람조차도 거의 해석 불가능할 정도다. 소스의 가독성도 문제지만 에러 메시지도 의미를 해석하기 쉽지 않다. 소스가 난해하기 때문에 팀 프로젝트에 불리하며 디버깅도 어렵고 유지 보수 비용이 증가한다.

컨테이너(Container)

똑같이 생긴 것들을 모아 놓는 장소라고 쉽게 생각할 수 있으며 다른 말로 컬렉션(Collection)이라고도 부른다. STL의 컨테이너는 자료를 저장하는 방식과 삽입, 정렬, 삭제하는 관리 방식에 따라 여러 가지가 있는데 크게 세 가지 부류로 구분된다.

① 시퀀스 컨테이너(Sequence Container) : 자료의 선형적인 집합이며 자료를 저장하는 기본 임무에 충실한 가장 일반적인 컨테이너이다. 삽입된 자료를 무조건 저장하며 입력되는 자료에 특별한 제약이나 관리 규칙은 없다. 사용자는 시퀀스의 임의 위치에 원하는 요소를 마음대로 삽입, 삭제할 수 있다. STL에는 벡터(vector), 리스트(list), 데크(deque) 세 가지의 시퀀스 컨테이너가 제공된다.

② 연관 컨테이너(Associative Container) : 자료를 무조건 저장하기만 하는 것이 아니라 일정한 규칙에 따라 자료를 조직화하여 관리하는 컨테이너이다. 정렬이나 해시 등의 방법을 통해 삽입되는 자료를 항상 일정한 기준(오름차순, 해시 함수)에 맞는 위치에 저장해 놓으므로 검색 속도가 빠른 것이 장점이다. 표준 STL에는 정렬 연관 컨테이너인 셋(set), 맵(map) 등의 컨테이너가 제공된다.

③ 어댑터 컨테이너 (Adapter Container) : 시퀀스 컨테이너를 변형하여 자료를 미리 정해진 일정한 방식에 따라 관리하는 것이 특징이다. 스택(stack), 큐(queue), 우선 순위 큐(priority_queue) 세 가지가 있는데 스택은 항상 LIFO의 원리로 동작하며 큐는 항상 FIFO의 원리로 동작한다. 자료를 넣고 빼는 순서를 외부에서 마음대로 조작할 수 없으며 컨테이너의 규칙대로 조작해야 한다.

시퀀스 컨테이너

벡터(vector)

타입 설명
value_type 컨테이너의 요소 타입이다.
(const_) pointer 요소를 가리키는 포인터 타입이다. 이하 4개의 타입은 상수 버전도 제공된다.
(const_) reference 요소의 레퍼런스 타입이다.
(const_) iterator 요소의 레퍼런스를 가리키는 반복자 타입이다.
(const_) reverse_iterator 역방향 반복자 타입이다.
difference_type 두 반복자의 차를 표현하는 타입이다. 통상 int이다.
size_type 크기를 표현하는 타입이다. 통상 unsigned이다.

벡터의 생성자는 다음 4 가지가 중복 정의되어 있다. 문서상의 원형에는 생성자의 마지막 인수로 const A& al = A()라는 할당기 인수가 하나 더 있는데 디폴트가 지정되어 있고 보통 생략하므로 원형에 적지 않기로 한다. 실용성도 없는데 괜히 원형만 복잡하게 만들 뿐이다. 이후의 컨테이너도 마찬가지로 할당기는 무시한다.

explicit vector(); 
explicit vector(size_type n, const T& v = T()); 
vector(const vector& x); 
vector(const_iterator first, const_iterator last); 
두 번째 생성자는 벡터의 초기 크기를 지정하며 T 타입의 초기값을 지정할 수 있다. 초기값의 디폴트는 T의 디폴트 생성자가 만든 값으로 지정되어 있는데 통상 0, false, NULL, "" 등이 될 것이다. 물론 어디까지나 디폴트일 뿐이므로 초기값을 명시하면 지정한 값 n개를 가지는 벡터가 만들어진다. 이 생성자는 속도를 높이기 위해 첫 번째 요소만 생성한 후 나머지 n-1개의 요소는 복사 생성자를 호출하여 생성한다.

두 번째 생성자는 정수값 하나만을 취하므로 변환 생성자이다. 그래서 explicit로 선언하여 명시적인 생성만을 허락한다. 만약 이 생성자가 explicit가 아니라면 vi=3 따위로 대입할 때 컴파일러는 정수 3으로부터 크기 3의 벡터를 생성하여 vi에 대입하려고 할 것이다. 또는 벡터를 인수로 취하는 함수에게 정수를 넘겨도 별다른 불만없이 정수로부터 벡터를 만들어 함수를 호출하려고 할 것이다. 정수와 벡터는 호환 타입이 아니므로 명시적으로 지정하지 않는 한 변환하지 않는 것이 바람직하며 그래서 이 생성자가 explicit로 선언되어 있는 것이다.

세 번째 생성자는 복사 생성자인데 다른 벡터로부터 똑같은 벡터를 만들어 낸다. 내부에서는 아마도 깊은 복사를 할 것이다. 네 번째 생성자는 반복자가 지정한 구간의 요소들을 가지는 새로운 벡터를 생성한다. 이때 반복자는 꼭 벡터의 반복자가 아니더라도 상관없다. 정적 배열이나 리스트의 반복자를 전달할 수도 있어 다른 컨테이너로부터 벡터를 초기화할 수 있다. 다음 예제는 벡터의 생성자 4 개를 테스트한다.

함수 설명
size 요소 개수를 조사한다.
max_size 벡터가 관리할 수 있는 최대 요소 개수를 조사한다.
capacity 할당된 요소 개수를 구한다.
resize(n) 크기를 변경한다. 새 크기가 더 클 경우 벡터의 원래 내용은 유지하며 새로 할당된 요소는 0으로 초기화된다.
reserve(n) 최소한의 크기를 지정하며 메모리를 미리 할당해 놓는다. 새 크기가 더 클 경우 벡터의 원래 내용은 유지한다. 새로 할당된 요소는 초기화되지 않는다.
clear(n) 모든 요소를 삭제한다.
empty 비어 있는지 조사한다.

resize는 지정한 크기로 요소 수를 늘리며 새로 생겨난 요소는 타입의 디폴트값으로 초기화되는데 vi는 정수형 벡터이므로 int()값인 0으로 초기화될 것이다.
capacity는 할당되어 있는 메모리양인데 이 크기는 size보다 크거나 같다. 벡터는 메모리가 부족할 경우 현재 용량의 2배씩 메모리를 늘려 나가며 앞으로 추가될 요소를 고려하여 약간의 여유분을 미리 할당해 놓는다.
reserve는 미리 메모리를 할당해 놓는 함수
clear는 벡터를 비우고
empty는 벡터가 비어 있는지 점검한다. empty는 size() == 0 조건을 점검하는데 이 두 조건이 논리적으로는 같지만 실제로는 굉장한 차이가 있을 수도 있다. size는 정확한 개수를 구하므로 요소가 많을 경우 일일이 세어 봐야 한다. 특히 리스트같은 컨테이너는 개수를 구하기 위해서도 순회가 필요하므로 size의 속도는 다소 느리다. 하지만 empty는 0인지 아닌지만을 점검하며 훨씬 더 빠른 속도로 컨테이너가 비어 있는지를 점검해내므로 가급적이면 empty를 사용하는 것이 유리하다.

삽입과 삭제
void push_back(const T& x); 
void pop_back(); 

push_front, pop_front 함수는 제공하지 않는다.
꼭 벡터의 중간에 요소를 삽입하고 싶다면 insert 함수를 사용한다. 벡터에 대해 push_front(V)를 하려면 insert(begin(),V)을 대신 호출하면 된다.

iterator insert(iterator it, const T& x = T()); 
void insert(iterator it, size_type n, const T& x); 
void insert(iterator it, const_iterator first, const_iterator last); 
insert는 삽입하기 전에 메모리가 부족할 경우 재할당하여 늘리고 it 이후의 요소를 뒤쪽으로 이동시킨다.

iterator erase(iterator it); 
iterator erase(iterator first, iterator last); 
반복자가 지정하는 요소 하나 또는 반복자 구간을 삭제할 수 있다.

insert와 erase는 요소를 관리하는 기본 동작이므로 대부분의 컨테이너에 동일한 이름과 형식으로 존재한다.

반복자 무효화 현상
삽입된 위치의 앞쪽은 무효화될 수도 있고 그렇지 않을 수도 있는데 재할당에 의해 메모리 번지가 바뀌면 무효화될 것이고 그렇지 않다면 영향을 받지 않을 것이다. 삽입에 의해 재할당이 자주 발생하지는 않지만 모든 경우에 유효성이 보장된다고 할 수는 없으므로 삽입하면 전 반복자가 무효화된다고 보는 것이 옳다.
삭제시는 삭제 구간의 뒤쪽은 무효화되지만 앞쪽은 영향을 받지 않는다. 삭제는 삽입과는 달리 메모리 재할당이 절대로 일어나지 않으므로 앞쪽 요소는 원래 자리를 항상 그대로 유지하기 때문이다.

컨테이너에 조금이라도 변형을 가했다면 이전에 조사해 놓은 반복자 전체를 믿지 않는 편이 더 확실하다.

연산자
대입

대입 연산자는 두 벡터를 완전히 똑같이 만드는데 만약 일부 구간만 복사하고 싶다면 assign 멤버 함수를 사용한다. 항상 이런 식인데 연산자는 전체에 대한 처리를 하며 일부분에 대한 처리는 별도의 멤버 함수가 준비되어 있다. 연산자는 전달받을 수 있는 피연산자 수가 제한되어 있어 부분에 대한 처리를 할만큼 충분한 정보를 제공받을 수 없기 때문이다. assign은 다음 두 개의 버전이 제공된다.

void assign(size_type count, const Type& val); 
void assign(InIt first, InIt last); 
첫 번째 버전은 val값 count개를 반복적으로 복사한다. 벡터를 특정값으로 가득 채우고 싶을 때 이 버전을 사용한다. 두 번째 버전은 반복자 구간을 받아들이는데 다른 컨테이너의 일부 요소를 벡터에 대입한다. 템플릿 함수로 정의되어 있으므로 입력 반복자 조건만 만족하면 벡터가 아닌 컨테이너의 구간도 대입할 수 있다.

교환

void swap(vector& Right); 
멤버 함수 외에 모든 컨테이너에 대해 쓸 수 있는 일반적인 swap 알고리즘도 있다. 벡터끼리 교환하고 싶을 때는 다음 두 가지 방법 중 하나를 사용한다.
v1.swap(v2);     // 멤버 함수 
swap(v1,v2);     // 알고리즘 함수 
그러나 실제로 두 개의 swap 함수는 완전히 동등한데 왜냐하면 swap 알고리즘 함수가 벡터에 대해 부분 특수화되어 있기 때문이다. swap은 두 개의 컨테이너를 교환하되 벡터나 리스트 등 더 빠르게 교환할 수 있는 컨테이너에 대해서는 멤버 함수 버전을 호출하도록 되어 있다.

요소 참조

const_reference at(size_type pos) const; 
reference at(size_type pos); 
] 연산자와 비슷하게 동작하는 at 함수도 정의되어 있는데 인수로 첨자를 지정한다. [
? 연산자는 첨자가 무조건 유효하다고 가정하는 반면 at 함수는 벡터의 크기를 점검하여 무효한 첨자일 경우 out_of_range 예외를 발생시킨다는 점이 다르다. 그래서 배열 범위를 벗어나는 실수를 막을 수 있다. [ ] 연산자와 at 함수 모두 상수, 비상수 버전이 각각 정의되어 있다.

사용자 정의 타입
벡터에 임의의 타입을 저장할 수 있지만 그렇다고 정말 아무 타입이나 저장할 수 있는 것은 아니며 일정한 조건을 만족하는 타입만 저장할 수 있다. 다음 예제는 내부에서 동적 할당을 하는 객체를 요소로 가지는 벡터를 만든다. 동적 할당을 하는 클래스는 생성자, 가상 파괴자, 복사 생성자, 대입 연산자를 적절히 정의해야 한다.

일반적인 객체는 대단히 클 수 있으므로 벡터에 직접 객체를 저장하는 것보다는 객체의 포인터를 저장하는 것이 성능상 유리하며 훨씬 더 일반적이다.

이정도는 해줘야... 디폴트 생성자, 변환 생성자, 복사 생성자, = 대입 연산자, ==, < 비교 연산자, 가상 파괴자

vector<bool>
C 구조체의 비트필드와 유사하다. vector<bool>은 진위형의 요소들을 1비트에 하나씩 압축하여 저장하는 별도의 독립 클래스이다. 8개의 진위형을 저장할 때 bool형의 단순 배열에 비해 vector<bool>의 크기가 훨씬 더 작다.

#include <iostream> 
#include <vector> 
using namespace std; 
  
void main() 
{ 
     vector<bool> vb(32); 
  
     cout << vb[10] << endl; 
     vb[10]=true; 
     cout << vb[10] << endl; 
  
     vector<bool>::reference r=vb[10]; 
     cout << r << endl; 
     r.flip(); 
     cout << r << endl; 
     vector<bool>::iterator it; 
     for (it=vb.begin();it!=vb.end();it++) { 
          cout << *it; 
     } 
} 
vector<bool>에 포함된 reference 타입은 벡터내의 한 요소, 그러니까 한 비트를 표현하는 클래스이다. 이 외에 비트를 뒤집는 flip, bool형으로 변환하는 캐스트 연산자, 비트를 반전하는 ~ 연산자, 대입 연산자 등이 정의되어 있다. 예제에서는 반복자로 전체를 순회하면서 출력해 보았다. vector<bool>은 표준에 포함되어 있기는 하지만 몇 가지 점에서 문제가 있고 컴파일러마다 지원 범위가 달라 가급적이면 사용을 자재하는 것이 좋다.

리스트(List)

리스트의 생성자를 연구해 보자. 리스트의 생성자는 모두 4개 제공되는데 벡터의 생성자와 원형이 동일하다. 똑같은 목적에 사용하는 컨테이너이므로 생성하는 방법부터 같을 수밖에 없다. 잘 사용되지 않는 할당기 인수는 원형에서 생략했다.

explicit list(); 
explicit list(size_type n, const T& v = T()); 
list(const list& x); 
list(const_iterator first, const_iterator last); 

삽입과 삭제
삽입, 삭제 함수도 벡터와 동일하다. 멤버 함수의 이름뿐만 아니라 원형까지 동일하며 사용 방법도 물론 똑같다.

iterator insert(iterator it, const T& x = T()); 
void insert(iterator it, size_type n, const T& x); 
void insert(iterator it, const_iterator first, const_iterator last); 
iterator erase(iterator it); 
iterator erase(iterator first, iterator last); 

다만 처리 속도는 벡터보다 훨씬 빠른데 위치와 요소 개수에 상관없이 상수 시간내에 삽입, 삭제된다. 속도가 빠르다는 것 외에도 반복자가 무효화되지 않는 장점도 있다. (단, 반복자가 가리키는 대상이 삭제되면 당연히 반복자는 무효화 된다.)

void remove(const Type& val); 
void remove_if(UniPred F) 
특정값을 가지는 요소를 모두 삭제하고 싶을 때는 다음 remove() 멤버 함수를 사용한다.

리스트에는 링크 재배치의 장점을 살릴 수 있는 여러 가지 멤버 함수들이 준비되어 있다.

void swap(list& Right); 
void reverse( ); 
void merge(list& Right); 
merge도 링크를 조작하여 한쪽 리스트를 다른 쪽의 끝과 연결하기만 하면 된다.

void splice(iterator it, list& x); 
void splice(iterator it, list& x, iterator first); 
void splice(iterator it, list& x, iterator first, iterator last); 
첫 번째 원형은 it 위치에 x리스트의 모든 요소들을 이동시킨다. 마치 작은 새끼줄을 큰 새끼줄의 허리 부근에 연결하는 것과 같다. 복사가 아닌 이동이므로 x에 있던 요소들은 모두 제거된다. x는 호출하는 객체와는 당연히 달라야 하는데 자신을 자신에게 이을 수는 없는 노릇이다.
두 번째 원형은 x의 first위치에 있는 요소 하나만 it위치로 이동시킨다.
세 번째 원형은 하나의 요소가 아니라 반복자 구간을 지정하여 일정 범위의 요소를 한꺼번에 이동시킨다는 점이 다르다. 같은 리스트내에서의 이동도 가능하므로 이 두 원형은 x가 호출하는 객체 자신이어도 상관없다.

전체를 이동시키는 splice(it, x)는 전체 구간을 이동하는 splice(it, x.begin(),x.end())와 동일한 함수라고 할 수 있다.

리스트의 정렬
리스트는 임의 접근 반복자를 제공하지 않으므로 정렬 속도가 대단히 느린 편이다. 그래서 sort 알고리즘 함수를 사용하지 못하며 대신 sort 멤버 함수를 사용해야 한다. sort 알고리즘은 임의 접근을 활용하는 퀵 소트로 구현되어 있는데 비해 리스트의 sort 멤버 함수는 리스트에 좀 더 특화된 알고리즘으로 작성되어 있다. 두 개의 원형이 제공된다.
void sort(); 
void sort(BinPred op); 
인수를 받지 않는 sort 멤버 함수는 < 연산자로 노드를 비교하여 정렬하며 조건자를 받아들이는 sort 멤버 함수는 조건자의 비교 결과대로 정렬한다.

다음 멤버 함수는 연속된 중복 요소를 제거하는데 같은 이름의 전역 함수도 있지만 멤버 함수는 리스트의 링크 구조를 활용하도록 구현되어 있다.

void unique(); 
void unique(UniPred op); 

속력 비교
컴파일러 비주얼 C++ 6.0 비주얼 C++ 8.0 Dev-C++
벡터 220 280 891
리스트 2924 1222 3455

데크(Deque)

조작 위치에 따라 약간의 속도차만 있을 뿐이므로 백터와 데크는 기능적으로 완전히 대체 가능하다. 앞 절에서 벡터로 만든 예제들은 거의 대부분 별다른 수정없이 데크로 바꿀 수 있는데 소스의 vector만 deque로 바꾸면 된다. 주로 뒤쪽에 추가만 한다면 벡터가 탁월한 선택이며 양쪽에서 추가, 삭제가 발생하면 데크가 더 적합하다.

연관 컨테이너

연관 컨테이너(Associative Container)는 키와 값처럼 관련이 있는 데이터를 하나의 쌍으로 저장하는 컨테이너이다.

연관 컨테이너를 구현하는 방법에는 균형 잡힌 이진 트리를 사용하여 정렬된 상태로 저장하는 방법이 있고 해쉬 테이블에 저장하는 방법이 있다. 해쉬는 해쉬 함수에 의해 한 번에 자료를 찾을 수 있으므로 검색 속도가 거의 순식간이라는 장점이 있지만 성능을 높이기 위해서는 해쉬 테이블이 충분히 커야 하므로 메모리 낭비가 조금 심한 편이다. 또한 검색이 빠르기는 하지만 데이터의 분포 정도에 따라 검색 속도가 운에 좌우되는 경향이 있어 최악의 상황에는 속도가 떨어질 수도 있다.

이진 트리를 사용하는 방법은 메모리의 낭비가 심하지 않고 데이터의 종류에 상관없이 항상 일정한 성능을 보장하므로 훨씬 더 안정적이다.
표준 STL은 이진 트리에 정렬하는 방식으로 구현된 연관 컨테이너만 제공한다. 해쉬 연관 컨테이너는 비록 표준은 아니지만 확장 라이브러리의 형태로 제공되는 것들이 많아 원한다면 언제든지 구해서 사용할 수 있다.

저장 대상 키 + 값
중복 불허
중복 허용 멀티셋 멀티맵

연관 컨테이너의 반복자는 모두 양방향 반복자이이다. 자료들이 항상 정렬된 상태를 유지하므로 다시 정렬할 필요가 없으며 검색이 워낙 빠르기 때문에 이분 검색을 할 필요도 없다. 언제든지 순서대로 순회하면 정렬된 결과를 얻을 수 있고 검색은 원래부터 초고속이므로 굳이 임의 접근 반복자가 필요하지도 않다.

연관 컨테이너들이 키와 값의 쌍을 표현하기 위해 사용하는 pair 구조체에 대해 알아보자
둘 이상의 값을 묶어서 관리하거나 리턴할 일이 많기 때문에 pair 구조체를 종종 사용한다.
실제 선언 형태는 컴파일러에 따라 달라진다.

pair 구조체
template<class T1, class T2> 
struct pair  
{ 
     typedef T1 first_type; 
     typedef T2 second_type; 
     T1 first; 
     T2 second; 
     pair() : first(T1()), second(T2()) {} 
     pair(const T1& v1, const T2& v2) : first(v1), second(v2) {} 
}; 

알다시피 함수는 한 번에 하나의 값만 리턴할 수 있으며 굳이 두 개의 값을 리턴하고 싶다면 참조 호출을 쓸 수 있지만 번거롭다. 두 값의 쌍을 포함하는 구조체를 정의하면 이 구조체를 리턴함으로써 포함된 값 두 개를 리턴하는 것과 같다. 구조체는 값으로 복사, 대입되므로 함수의 리턴 타입으로 사용할 수 있다.

pair 구조체를 사용하려면 utility 헤더 파일을 포함해야 하는데 STL의 다른 헤더 파일을 인클루드하면 utility 헤더 파일도 자동으로 인클루드된다.

셋(Set)

셋(Set)은 영어 단어 뜻 그대로 집합을 의미하는데 동일한 타입의 데이터를 모아 놓은 것이다. 저장하는 데이터 자체가 키로 사용되며 값은 저장되지 않는다. 동일 타입의 집합이라는 면에서는 벡터와 같지만 아무 위치에나 삽입되는 것이 아니라 정렬된 위치에 삽입된다는 점이 다르며 그래서 검색 속도가 아주 빠르다.

template<class Key, class BinPred = less<Key>, class A = allocator<Key> > 
class set { ... } 

비교 객체와 할당기는 대개의 경우 생략하므로 셋 타입을 만들기 위해 꼭 필요한 것은 키의 타입밖에 없는 셈이다. set<int>는 정수형의 집합이며 set<Time>은 Time 객체의 집합이다. 연관 컨테이너도 시퀀스와 마찬가지로 내부에서 사용하는 타입을 정의하는데 value_type, iterator, const_iterator, reference 등의 같은 이름을 사용한다. 이 외에 다음 세 개의 타입을 추가로 정의한다.
타입 설명
key_type 키의 타입이다. 셋은 키가 곧 값이므로 value_type과 동일하며 맵은 키와 값의 pair 타입으로 정의된다.
key_compare 키를 비교하는 함수 객체 타입이다. 디폴트는 less로 되어 있다.
value_compare 값을 비교하는 함수 객체 타입이다. 셋에서는 key_compare와 동일한 타입으로 정의되며 맵에서는 pair를 비교한다.

생성자
explicit set(const BinPred& comp = BinPred()); 
set(const set& x); 
set(InIt first, InIt last, const BinPred& comp = BinPred()); 
첫 번째 생성자가 디폴트 생성자인데 요소를 가지지 않는 빈 셋을 만든다.
두 번째 생성자는 복사 생성자이며
세 번째 생성자는 반복자 구간의 요소들로 셋을 생성한다. 만약 중복된 키가 있다면 이때는 한 번만 삽입되며 멀티셋이라면 전부 삽입될 것이다.

삽입과 삭제
셋에 키를 삽입할 때는 insert 멤버 함수를 사용한다. 세 가지 버전이 제공된다.

pair<iterator, bool> insert(const value_type& val); // 셋의 경우 
iterator insert(const value_type& val); // 멀티셋의 경우 
 
iterator insert(iterator it, const value_type& val);  
void insert((iterator first, iterator last);  
셋의 경우 키가 중복될수 없으므로, 삽입이 실패할 수도 있다. 그래서 pair구조체의 두번째 인수로 참거짓이 넘어온다.
멀티셋의 경우 키가 중복될 수 있으므로 언제나 삽입은 성공한다. 그래서 반환값은 iterator 뿐이다.

나머지 3번째 4번째 insert함수는 셋과 멀티셋의 원형이 동일하다.
3번째 함수는 하나를 삽입하는데 위치는 크게 관계가 없다. 단지 빠른 삽입을 위한 힌트 역활을 할 뿐이다.
반환값은 셋의 경우 중복되면 실패하는데 이럴경우 이전에 존재하는 val의 반복자를 반환한다.
성공한다면 당연히 삽입된 val에 대한 반복자를 반환한다.

4번째 함수는 셋의 경우 중간에 실패하더라도 건너띄고 다음놈을 삽입한다.
그외의 경우는 상상에 맡긴다-_- 뻔하자나.

iterator erase(iterator it); 
iterator erase(iterator first, iterator last); 
size_type erase(const Key& key); 
세 가지 방식으로 삭제할 수 있는데 각각 반복자가 지정하는 요소 하나, 반복자 구간의 모든 요소 또는 특정한 키를 가지는 요소를 찾아서 삭제한다. 키를 지정하는 erase의 경우 지정한 키가 셋에 포함되어 있지 않으면 아무 것도 하지 않으며 멀티셋의 경우 같은 키값을 가지는 모든 요소를 한꺼번에 삭제한다.

전역 find 함수는 셋의 모든 요소를 순회하면서 순차적으로 검색하지만 find 멤버 함수는 정렬되어 있다는 특성을 이용하여 이진 트리 검색을 하므로 훨씬 더 빠르다.

멀티셋의 경우 find 멤버 함수로 찾으면 중간 중간을 쿡쿡 찔러가며 검색하므로 중복된 키 중 어떤 것이 검색될 지 알 수 없는데 이는 이진 검색의 일반적인 특성이다. 반면 find 전역 함수는 처음부터 순서대로 검색하므로 항상 첫 번째 요소가 검색된다. 중복된 키 중 첫 번째 또는 마지막 요소를 찾고 싶으면 lower_bound, upper_bound 함수를 사용해야 하며 둘을 한꺼번에 조사하고 싶으면 equal_range 멤버 함수를 사용한다. equal_range는 처음과 끝 반복자의 쌍을 조사하여 리턴한다.

 
iterator lower_bound(const Key& _Key); 
iterator upper_bound(const Key& _Key); 
pair <iterator, iterator> equal_range (const Key& _Key); 

객체의 셋
셋의 디폴트 비교 함수 객체인 less가 요소 객체의 < 연산자로 비교를 수행하므로 일단은 < 연산자만 정의되어 있으면 된다.

함수 객체를 사용할 수도 있는데 처음 생성시 두번째 템플릿 인수로 넣어주면 된다.

set<Time, greater<Time> > Times; // 이런 형식 

객체의 포인터 집합 셋에 임의의 타입을 요소로 저장할 수 있다고 했으므로 객체의 포인터도 셋에 저장할 수 있다. 포인터는 기본적으로 비교 연산을 제공하므로 셋의 요소가 되기 위한 요건을 만족하기는 하지만 객체의 번지를 기준으로 정렬하는 것은 실용성이 없으며 포인터가 가리키는 곳의 실제 객체를 기준으로 정렬해야 한다. 이때도 사용자가 비교 함수 객체를 직접 제공해야 한다.

동등성과 동일성
<= 연산은 확정적이지 못하므로 셋이 정신을 못차리고 엉뚱한 비교를 해 대며 그러다 보니 같은 값도 다르다고 판단하여 중복 삽입되는 것이다. 그래서 셋은 < 아니면 > 두 연산자 중 하나로만 비교해야 한다.

■ 동일성(equality) : 두 값이 완전히 같은지 검사한다. 객체의 경우 모든 멤버가 일치해야 동일하며 주로 == 연산자를 사용한다.
■ 동등성(equivalance) : 두 값이 같은 값으로 인정되는지 검사한다. 키에 해당하는 값만 비교하며 < 연산자의 조합으로 정의된다.

동등성은 두 값이 집합의 기준에서 볼 때 같다고 인정된다는 뜻이다. STL에서 두 키 a와 b의 동등성은 다음 조건으로 점검한다.

!(a < b) && !(b < a) 

디폴트인 less일 때의 얘기이고 좀 더 일반적으로 얘기하자면 비교 객체가 Comp일 때 다음 조건을 만족해야 두 키가 동등하다고 한다.

!Comp(a,b) && !Comp(b,a) 

순서를 바꿔가며 비교해 봐도 둘 다 거짓일 때만 동등하다. 셋의 insert 함수는 삽입 위치를 결정하기 위해 동등성의 개념을 사용한다.

멤버함수 find()는 동등성을 비교하지만
전역함수 find()는 동일성을 비교한다. 주의하자.

키 변경 불가 원칙
키는 셋이 요소의 순서를 정하기 위해 사용하는 중요한 값이므로 셋에 이미 들어가 있는 키를 변경해서는 안된다.
만약 키를 꼭 바꾸고 싶다면 어떻게 해야 할까? 이때는 원하는 요소를 일단 삭제하고 키를 변경한 후 다시 삽입하는 방법밖에 없다.

집합 연산
집합 연산 알고리즘은 합집합, 교집합, 차집합, 포함 여부 등의 집합 관련 연산 기능을 제공한다. 전역 알고리즘 함수들이라 임의의 컨테이너와 함께 사용할 수 있지만 주로 셋과 함께 사용되므로 여기서 알아보도록 하자. 다음 다섯 가지의 함수가 제공된다.

OutIt set_union(InIt1 first1,InIt1 llast1, InIt2 first2, InIt2 last2, OutIt result); 
OutIt set_intersection (InIt1 first1,InIt1 llast1, InIt2 first2, InIt2 last2, OutIt result); 
OutIt set_difference (InIt1 first1,InIt1 llast1, InIt2 first2, InIt2 last2, OutIt result); 
OutIt set_sysmmetric_difference (InIt1 first1,InIt1 llast1, InIt2 first2, InIt2 last2, OutIt result); 
bool includes (InIt1 first1,InIt1 llast1, InIt2 first2, InIt2 last2); 
대표적으로 합집합을 구하는 set_union 함수에 대해서만 알아보자. 이 함수는 두 개의 반복자 구간 first1~last1, first2~last2를 인수로 전달받아 두 구간에 속하는 요소들을 중복없이 합쳐서 result 반복자 위치에 대입한다. 원본 반복자 구간은 입력 반복자이므로 꼭 셋일 필요는 없지만 효율적인 합집합 연산을 위해 반드시 정렬되어 있어야 한다. 정렬된 벡터나 리스트의 구간도 원본으로 사용할 수 있다.

구해진 합집합은 result 반복자가 지정하는 위치에 차례대로 대입되는데 이 반복자도 출력 기능만 제공하면 되므로 임의의 컨테이너에 결과를 저장할 수 있다. 이때 결과를 대입받을 컨테이너는 예상되는 합집합을 저장할만한 충분한 공간을 확보하고 있어야 한다. 아니면 삽입 반복자를 사용하여 입력되는데로 컨테이너의 공간을 재할당하도록 할 수 있는데 주로 끝에 삽입하는 back_inserter가 사용된다.

맵(Map)

셋이 키의 집합만을 관리하는데 비해 맵은 키와 값의 쌍을 관리한다.
template<class Key, class T, class BinPred = less<Key>, class A = allocator<T> > 
class map { ... } 
키와 값의 타입은 디폴트가 없으므로 반드시 지정해야 한다.

맵이 정의하는 타입은 셋과 동일하되 key_type과 value_type이 다르게 정의되어 있으며 key_compare와 value_compare도 다른 함수 객체이다. 셋은 키만 있고 키가 값을 겸하는 것으로 취급하므로 value_type이 key_type과 같지만 맵은 이 둘이 각각 따로이므로 다른 타입이다. 맵의 value_type은 키와 값의 pair타입으로 정의되며 value_compare는 키값만으로 비교를 수행하는 함수 객체 타입이다. 맵의 생성자는 셋의 생성자와 완전히 동일하다.

explicit map(const BinPred& comp = BinPred()); 
map(const map& x); 
map(InIt first, InIt last, const BinPred& comp = BinPred()); 
세 번째 생성자는 반복자 구간으로부터 맵을 생성하는데 다른 컨테이너의 구간으로부터 생성가능하다. 그러나 타입이 키와 값을 쌍으로 가지는 pair여야 하므로 실제로는 같은 타입의 맵끼리만 구간 복사가 가능하다고 할 수 있다. 구간 복사시 정렬되어 들어가며 멀티 맵이 아니면 중복된 키는 한 번만 삽입된다.

삽입과 삭제
pair<iterator, bool> insert(const value_type& val); 
iterator insert(iterator it, const value_type& val); 
void insert((iterator first, iterator last); 
셋과 원형이 완전히 일치한다. 공통적이므로 인터페이스가 동일할 수밖에 없다.

맵에 삽입되는 요소의 타입인 value_type은 키와 값을 쌍으로 가지는 pair객체여야 한다. 미리 pair 객체를 준비해 놓고 삽입할 수도 있겠지만 보통은 pair의 생성자를 호출하여 임시 pair객체를 만들어 곧바로 맵에게 전달한다. 예를 들어 문자열과 정수의 쌍을 삽입한다면 다음과 같이 한다.

m.insert(pair<string, int>("문자열",1234)); 

삽입되는 요소가 pair라는 것만 빼고 사용 방법은 셋의 insert 함수와 완전히 동일하다.

맵은 insert 함수외에 좀 더 편리한 삽입 방법을 제공하는데 [ ] 연산자와 대입 연산자로도 요소를 삽입할 수 있다.

T& operator[](const Key& k); 
[ ] 연산자는 인수로 전달된 k를 키로 가지는 pair 객체를 생성하여 맵에 삽입하고 이 pair의 값(pair.second)에 대한 레퍼런스를 리턴한다. 레퍼런스에 값을 곧바로 대입하기만 하면 방금 삽입한 요소의 값을 지정할 수 있다.

위의 insert()문은 밑이 대입문과 동일한 방법이다.

m["문자열"]=1234; 

iterator erase(iterator it); 
iterator erase(iterator first, iterator last); 
size_type erase(const Key& key); 
erase 함수를 사용하는데 이 함수도 셋과 완전히 일치한다. 반복자가 지정한 요소 하나, 반복자 구간의 모든 요소, 특정한 키를 가지는 요소를 삭제할 수 있다. 멀티 맵의 경우 키를 가진 요소가 여러 개 발견되면 전부 삭제된다.

검색
맵을 검색할 때는 find 멤버 함수를 사용한다. 상수 버전과 비상수 버전이 중복 정의되어 있다.

 
iterator find(const Key& val); 
const_iterator find(const Key& val) const; 

수정
[ ] 연산자는 키가 없으면 삽입하지만 이미 존재할 경우는 해당 요소의 값에 대한 레퍼런스를 리턴한다. 따라서 존재하는 키에 대해서는 값을 수정하는 용도로 사용할 수 있다.

맵의 키를 수정하는 코드는 컴파일 에러로 처리된다. 맵의 요소 타입이 pair(Key, T)가 아니라 pair(const Key, T)로 되어 있어 키는 무조건 수정 불가능하다. 키는 오로지 정렬 기준으로만 사용되므로 일단 삽입되면 절대로 수정할 수 없다.

컨테이너 어댑터

컨테이너 어댑터는 기존 컨테이너의 기능 중 일부만을 공개하여 기능이 제한되거나 변형된 컨테이너를 만든다.

스택

template<class T, class Cont=deque<T> > 
class stack { ... } 

스택이 제공하는 타입은 size_type, value_type, container_type 세 가지 밖에 없다. 이중 container_type은 스택이 자료 관리를 위해 사용하는 기본 컨테이너의 타입이며 템플릿 인수 Cont와 같은 타입이다. 생성자는 다음 하나밖에 없다.

explicit stack(const allocator_type& al = allocator_type()); 

인수로 할당기를 받을 수 있지만 디폴트가 지정되어 있으므로 결국 이 생성자는 인수를 취하지 않는 디폴트 생성자라고 할 수 있다.

void push(const T& x); 
void pop(); 
value_type& top(); 
const value_type& top() const; 
스택의 고유한 동작은 push, pop, top 세 가지 뿐이다. 그 외의 불필요한 연산, 예를 들어 중간에 삽입, 삭제한다거나 구간 복사를 하는 기능은 숨겨서 쓸 수 없도록 되어 있다. 중간에 삽입, 삭제가 가능하면 그것은 더 이상 스택이 아니다. top은 상수, 비상수 버전이 따로 제공된다.

시스템 스택을 비롯한 일반적인 스택은 값을 읽는 함수와 버리는 함수가 따로 제공되지 않으며 pop 함수를 호출하면 제일 상단의 값을 제거하면서 리턴하도록 되어 있는데 STL의 스택은 읽는 함수와 제거하는 함수가 분리되어 있다.
만약 pop이 상단 요소를 제거함과 동시에 레퍼런스를 리턴한다고 해 보자. 이렇게 되면 이미 제거되어 버린 요소를 가리키는 무효한 레퍼런스를 리턴하는 셈이므로 논리적으로 모순이다.

상단의 값을 읽고 버리려면 일단 top으로 먼저 읽어서 사용하고 pop으로 빼서 버리는 것은 따로 해야 한다.

큐(Queue)

큐는 스택에 비해 먼저 들어간 값이 먼저 나오는(FIFO) 자료 구조이다. 자료를 관리하는 기본적인 기능은 시퀀스의 것을 그대로 재사용할 수 있으며 FIFO 원리대로 동작하기 위해 꼭 필요한 인터페이스는 다음 4가지 밖에 없다.

push, pop : 앞뒤에서 값을 추가하거나 제거한다.
front, back : 앞뒤의 값을 읽는다.

template<class T, class Cont=deque<T> > 
class queue { ... } 

우선순위 큐(Priority Queue)

우선 순위 큐는 벡터와 유사하되 값을 빼 낼 때 가장 큰 값을 리턴한다는 점이 다르다.
필요한 동작은 push, pop, top 세가지밖에 없다.

템플릿 선언은 다음과 같다.

template<class T, class Cont = vector<T>, class BinPred = less<Cont::value_type> > 
class priority_queue { ... } 
BinPred
?는 값의 비교에 사용할 비교 함수 객체이되 디폴트는 less로 되어 있으므로 큰 값이 가장 먼저 나온다. 생성자는 다음과 같다.

explicit priority_queue(const BinPred& pr = BinPred()); 
priority_queue(const value_type *first, const value_type *last, const BinPred& pr = BinPred()); 
생성자이다.

반복자(Iterator)

순회 방법을 일반화하기 위해 STL에서 사용하는 개념이 바로 반복자이다. 어떤 경우에는 포인터가 되기도 하고 어떤 경우에는 첨자가 되기도 하며 또 어떤 경우에는 다소 복잡한 객체가 요구되기도 한다. 반복자는 다음과 같은 기능을 가지는데 종류에 따라서는 일부 기능이 제외되기도 한다.

① 컨테이너의 요소 하나를 가리키는 기본적인 역할을 한다.
② 가리키는 지점의 요소를 읽고 쓸 수 있다. 내용을 읽는 * 연산자가 정의된다.
③ 증감에 의해 주변 요소로 이동할 수 있다. ++, -- 등의 연산자가 정의된다.
④ 반복자끼리 대입, 비교 가능해야 한다. 대입, 비교 연산자가 정의된다.

반복자에 대해 마지막으로 주의해야 할 점은 STL 알고리즘은 반복자의 유효성을 전혀 점검하지 않으며 항상 유효하다고 가정한다는 점이다. 반복자가 틀린 위치를 가리키고 있을 경우의 결과는 정의되어 있지 않은데 재수 없으면 다운될 수도 있다. STL도 결국 범위를 점검하지 않는 C언어의 한계는 벗어나지 못했는데 그 이유는 물론 성능이다. 반복자를 참조할 때마다 범위를 일일이 점검하다가는 엄청난 희생을 감수해야 한다. 그러나 반복자는 보통 증가, 감소에 의해 구간 내에서만 움직이므로 큰 문제가 되지는 않는다.

반복자의 종류

반복자 약어 설명
입력 InIt? 오로지 입력만 가능하며 쓸 수는 없다.
출력 OutIt? 출력만 가능하며 읽지는 못한다.
순방향 FwdIt? 입출력이 모두 가능하다. 전진만 가능하다.
양방향 BiIt? 앞뒤로 이동할 수 있다. 감소 연산자를 정의한다.
임의 접근 RanIt? 임의의 요소를 참조할 수 있다. [ ] 연산자를 정의한다.

예)

InIt find(InIt first, InIt last, const T& val); 
void sort(RanIt first, RanIt last); 

입출력 반복자
입력 반복자(Input Iterator)는 가장 기본적인 기능만 제공하는 반복자이다. 반복자이므로 컨테이너의 한 위치를 가리키는 것은 당연하고 반복자가 가리키는 위치의 요소를 * 연산자로 읽을 수도 있다. 읽는 것만 가능하므로 * 연산자를 사용하더라도 요소의 값을 변경하는 것은 불가능하다. 입력 반복자에 * 연산자를 적용한 결과는 우변값일 뿐이며 좌변값은 아니다. 즉, *입력 반복자 표현식은 읽기 전용이다.

전위형, 후위형의 ++ 연산자를 사용하여 다음 요소로 이동하는 기능도 가지고 있으며 같은 타입의 반복자와 상등 비교도 가능하다.

출력 반복자는 * 연산자를 사용하여 요소의 내용을 변경할 수 있는 반복자이다. 쓰기가 가능하다면 보통 읽기도 가능하지만 읽는 기능은 없어도 상관없다. 쓰기만 가능하면 출력 반복자라고 할 수 있으며 읽기 기능이 필수는 아니다. 다음 요소로 이동하는 ++연산자도 지원해야 한다. 그러나 출력은 입력과는 달리 무조건적이어서 범위 점검을 위한 ==, != 연산자는 필수가 아니다. 즉, 전진하면서 기록 가능하다는 조건만 만족하면 출력 반복자라고 할 수 있다.

반복자 구간끼리 복사하는 copy 알고리즘에 입력, 출력 반복자가 동시에 나타난다. 이 함수의 구현 코드는 다음과 같을 것이다. 물론 실제 코드는 컴파일러마다 다르다.

OutIt copy(InIt first, InIt last, OutIt result) 
{ 
     while (first != last)  
     { 
          *result++=*first++; 
     } 
     return result; 
} 

입력, 출력 반복자의 일종인 입출력 스트림 반복자는 콘솔에 연결된 반복자이다. 통상 표준 입출력 객체인 cin, cout에 대해 사용하지만 임의의 스트림 객체에 대한 반복자로도 사용할 수 있다. i(o)stream_iterator 클래스로 정의되어 있으며 이 반복자를 사용하면 표준 입력(키보드)으로 입력받은 내용을 순회하면서 읽어낼 수 있고 표준 출력(모니터)으로 문자를 출력할 수도 있다. 다음 예제는 리스트의 요소들을 화면으로 출력한다.

#include <iostream> 
#include <list> 
using namespace std; 
  
void main() 
{ 
     int ari[]={1,2,3,4,5}; 
     list<int> li(&ari[0],&ari[5]); 
 
     ostream_iterator<int> oit(cout,","); 
     copy(li.begin(),li.end(),oit); 
} 
리스트의 정수 요소들이 모두 출력되었으며 숫자들 사이에는 구분자인 쉼표가 하나씩 삽입되었다. oit 생성자의 두 번째 인수를 "\n"으로 변경하면 매 요소를 출력한 후 개행될 것이다. 컨테이너의 반복자 구간과 출력 스트림 반복자로 copy 함수를 호출하는 방법은 컨테이너를 화면에 덤프하는 가장 편리한 방법이며

#include <iostream> 
#include <vector> 
#include <algorithm> 
using namespace std; 
  
template<typename C>  
void dump(const char *desc, C c) 
{ 
     cout.width(12); 
     cout << left << desc << "==> "; 
     copy(c.begin(),c.end(),ostream_iterator<typename C::value_type>(cout," ")); 
     cout << endl; 
} 
  
void main() 
{ 
     vector<int> vi(16); 
     istream_iterator<int> iit(cin); 
     copy(iit,istream_iterator<int>(),vi.begin()); 
     dump("입력 완료 후",vi); 
} 
istream_iterator는 템플릿 인수로 입력받을 타입을 지정하며 생성자로는 입력 스트림 객체를 지정한다. 예제의 iit는 정수형을 cin으로부터 입력받는 객체로 선언되었다. istream_iterator의 디폴트 생성자는 스트림 끝을 나타내는 반복자를 생성하는데 이 반복자는 키보드 입력 종료를 의미한다.

입출력 스트림 반복자에는 이 외에도 좀 더 저수준의 i(o)strembuf_iterator 클래스도 있는데 버퍼를 직접 조작하며 문자 단위로 입출력을 수행하므로 성능이 훨씬 더 좋다.

순방향, 양방향 반복자
순방향 반복자(Foward Iterator)는 입력 출력이 모두 가능한 반복자이다. 입력, 출력 반복자는 둘 중 하나만 가능한데 비해 순방향 반복자는 읽기, 쓰기가 모두 가능하다. 이름이 의미하듯이 순방향으로 이동 가능하다는 조건도 포함된다. 순방향으로 이동 가능하다는 것은 ++ 연산자를 정의한다는 뜻인데 전위형, 후위형 모두 정의되어 있으므로 ++it, it++ 두 형태를 자유롭게 사용할 수 있다. 그러나 -- 연산자가 정의되어 있지 않으므로 역방향으로는 이동할 수 없다.

양방향 반복자(Bidirectional Iterator)는 역방향으로도 이동이 가능한 반복자이다. 순방향 이동을 위한 ++ 연산자는 물론이고 역방향의 이동을 위한 -- 연산자도 전위형, 후위형으로 각각 구현되어 있다. 역방향으로 검색한다거나 모든 요소의 순서를 반대로 뒤집을 때는 앞뒤로 자유롭게 이동해야 하므로 역방향 반복자가 필요하다. STL 컨테이너 중 이중 연결 리스트로 구현되어 있는 list가 양방향 반복자를 제공한다. 양방향 반복자는 순방향 반복자의 기능을 포함한다.

다음 두 알고리즘은 각각 순방향 반복자와 양방향 반복자를 요구한다. 함수의 원형에 FwdIt?, BiIt?라는 반복자 타입이 분명히 명시되어 있다.

void replace (FwdIt first, FwdIt last, const Type& Old, const Type& New); 
void reverse(BiIt first, BiIt last); 

임의 접근 반복자
임의 접근 반복자(Random Iterator)는 최상위 레벨의 반복자이며 제공하는 기능이 가장 많다. 양방향 반복자의 모든 기능을 포함하므로 * 연산자로 읽기, 쓰기와 ++, -- 연산자로 앞 뒤로 이동하기, 같은 위치를 여러 번 액세스 하기 기능을 제공한다. 여기에 임의 위치로 이동할 수 있는 기능이 추가로 제공되는데 거리에 상관없이 항상 상수 시간내에 이동 가능하다.

임의 위치로 곧장 이동할 수 있다는 것은 반복자에 임의의 정수를 더하는 +n 연산을 제공한다는 뜻이다. 포인터에 +n 연산을 적용하면 n*sizeof(타입) 만큼 즉시 이동하는데 임의 접근 연산자가 바로 이 기능을 제공하며 +n에 의해 현재 위치에서 n만큼 떨어진 요소로 곧장 이동할 수 있다.

복합 대입 연산자 +=, -=도 지원되는데 모두 +n으로부터 파생되는 연산들이다.

삽입 반복자
기존의 대입연산을 고치지 않고 반복자를 교환해 줌으로 대입이 삽입으로 변한다.

일반 반복자가 덮어쓰기 모드로 동작하는데 비해 삽입 반복자는 삽입모드로 동작한다.

삽입되는 위치와 내부 호출되는 함수에 따라 3종류의 삽입반복자로 나뉜다.
1. insert_iterator<Container> : 모든 컨테이너에서 사용가능하고 insert() 멤버함수를 사용해 중간에 삽입한다.
2. front_insert_iterator<Container> : 리스트와 덱에 사용가능하고 벡터는 안된다. push_front() 멤버함수를 사용해 선두에 삽입한다.
3. back_insert_iterator<Container> : 모든 컨테이너에서 사용가능하고 push_back() 멤버함수를 사용해 끝에 추가한다.

다음과 같은 예제에서 인덱스 2번위치에 대입연산은 삽입이 된다.

insert_iterator<vector<int> > insit(vi, vi.begin()+2); 
*insit = 99; 
*insit = 100; 

삽입 반복자 선언하는 문장이 다소 복잡하게 느껴진다면 삽입 반복자를 생성하여 리턴하는 템플릿 함수가 있다.

insert_iterator<Container> inserter(Container&Cont, Iterator it); 
front_insert_iterator<Container> front_inserter(Container& Cont); 
back_insert_iterator<Container> back_inserter(Container& Cont); 

상수 반복자
비상수 반복자는 각 컨테이너별로 iterator라는 타입으로 정의되는데 비해 상수 반복자는 const_iterator 타입으로 정의되어 있다.
단, 상수 반복자가 가리키는 대상이 상수이지 반복자 그 자체가 상수인 것은 아니므로 전후로 이동하여 다른 요소를 가리키는 것은 가능하다. const char *로 선언된 p 포인터로 문자를 바꿀 수 없지만 p 자체는 다른 문자로 이동 가능한 것과 마찬가지이다.

비상수는 항상 상수에 대입할 수 있으므로 설사 비상수 컨테이너의 반복자라 하더라도 상수 반복자에 곧바로 대입할 수 있다. 따라서 다음 대입문은 문법적으로 합당하다.

find 함수는 값을 검색하기 위해 읽기만 하므로 상수 반복자와 함께 사용되어도 아무 문제가 없다. 그러나 sort는 값을 교환하기 위해 반복자에 값을 쓰는데 상수 반복자가 이 기능을 제공하지 않으므로 컴파일 에러로 처리된다. 타입이 불일치하므로 잘못된 동작을 컴파일 중에 즉시 알아낼 수 있다.

역방향 반복자
역방향 반복자는 순회 방향이 거꾸로 되어 있는 반복자이다. 양방향 반복자나 임의 접근 반복자에 어댑터를 적용하여 구현되며 ++, -- 연산이 반대 방향으로 정의되어 있다. 각 컨테이너는 다음 두 개의 역방향 반복자 타입을 제공하므로 이 타입의 반복자를 선언하기만 하면 된다. 상수, 비상수 버전이 따로 준비되어 있다.

reverse_iterator 
const_reverse_iterator 

rbegin ~ rend로 역방향 순회를 하려면 rbegin은 끝요소를 가리키고 rend는 첫 요소보다 하나 더 앞쪽을 가리켜야 한다. 물론 rend위치는 실제로 컨테이너의 내부도 아니며 처리 대상에도 포함되지 않는다.

역방향 반복자에는 원래의 순방향 반복자를 리턴하는 base 멤버 함수가 정의되어 있다. 삽입, 삭제 함수들은 역방향 반복자를 받아들이지 않기 때문에 역방향 검색한 위치에 대고 어떤 작업을 하고 싶을 때는 순방향 반복자로 바꾸어야 한다. 또한 역방향 검색 후에 다시 순방향으로 검색하고 싶을 때 base로 언제든지 순방향 반복자를 구할 수 있다. base로 구한 순방향 반복자는 역방향 반복자보다 항상 하나 더 큰 값을 가진다.

이렇게 되어 있는 이유는 순방향 반복자가 역방향 반복자보다 한칸 더 오른쪽에 있으며 이렇게 해야 rbegin의 base가 end가 되기 때문이다. 다음은 역방향 검색 후 다시 순방향으로 검색한다.

vector<char>::reverse_iterator rit; 
vector<char>::iterator bit,it; 
rit=find(vc.rbegin(),vc.rend(),'t'); 
bit=rit.base(); 
it=find(bit,vc.end(),'a'); 
... rit rit.base() ...

문자열 끝에서부터 't'를 찾기 위해 역방향 검색을 했으며 't' 이후 최초로 나타나는 a를 찾기 위해 순방향 반복자 bit를 rit.base로 구하여 bit이후부터 검색했다.

반복자와 알고리즘
입력 반복자
==, !=, *로 읽기, ++로 전진이동

출력 반복자
*로 쓰기, ++로 전진 이동

순방향 반복자 : (상속) 입력반복자, 출력 반복자
*로 읽고 쓰기
++로 전진 이동

양방향 반복자 : (상속) 양방향 반복자
--로 후진 이동

임의접근 반복자 : (상속) 양방향 반복자
+n, [], +=, -=
반복자끼리 뺄셈 : -
반복자끼리 비교 : <, >

반복자와 속성
http://winapi.co.kr/clec/cpp4/39-2-6.htm

알고리즘(Algorithm)

STL의 알고리즘 함수들은 대부분 특정 컨테이너의 멤버 함수가 아닌 일반 전역 함수로 작성되어 있다. 왜냐하면 반복자에 의해 컨테이너를 다루는 방법이 일반화되어 모든 컨테이너에 대해 적용할 수 있으므로 멤버 함수가 될 필요도 없고 멤버 함수가 되어서도 안된다. STL은 일반화된 알고리즘을 제공하므로 캡슐화를 적극적으로 사용하지 않는다. 컨테이너 클래스들은 표현하고자 하는 자료 구조를 위한 데이터 멤버와 연산자만을 정의하며 알고리즘 구현은 반복자와 전역 함수에게 맡긴다.

함수 설명
count 조건에 맞는 요소의 개수를 센다.
for_each 각 요소에 대해 지정한 작업을 한다.
equal 구간이 일치하는지 비교한다.
search 일치하는 부분 구간을 검색한다.
copy 구간끼리 복사한다.
fill 일정한 값으로 지정 구간을 채운다.
reverse 구간의 요소들을 반대로 뒤집는다.
random_shuffle 요소들을 무작위로 섞는다.
swap 컨테이너를 교환한다.
binary_search 이분 검색한다.
merge 구간을 병합하여 새로운 구간으로 복사한다.
accumulate 구간의 값을 모두 합한다.

함수객체(Functor)

STL의 알고리즘은 전역함수가 처리하며 문제를 풀기 위한 반복자 구간, 검색 대상, 채울 값 따위의 정보들이 함수의 인수로 전달된다. 어떤 함수들은 내부에서 모든 동작을 다 처리하지 않거나 특성상 할 수 없는 경우가 있다. 검색 하고자 하는 값이 정확하게 어떤 조건인지, 정렬을 위해 요소를 비교할 때 어떤 방식으로 비교할 것인지 함수가 마음대로 정할 수 없다.
이때 함수에게 좀 더 구체적인 처리 방식을 지정하기 위해 사용자가 미리 만들어 놓은 함수 객체를 전달한다.
알고리즘 함수는 동작 중에 사용자의 개입이 필요한 부분에 대해서 함수 객체를 호출하여 의사를 결정한다.
마치 C표준라이브러리의 qsort 함수가 정렬을 위한 비교를 위해 사용자 정의 함수를 함수 포인터로 호출하는 것과 같다.

함수객체는 함수 호출 연산자인 ()을 오버로딩한 객체를 의미하는데 이 연산자를 통해 마치 함수를 호출하듯이 객체를 호출할 수 있다.

함수 포인터에 비해 함수객체가 가지는 장점
① 함수객체는 인라이닝되어 클래스 내부의 멤버함수는 호출부의 코드가 실제 생성될때 인라인으로 확장된다. 함수포인터는 번지의 주소값을 가지는 변수이므로 인라인화 될 수 없다.

② 함수객체는 그 자체로 객체이므로 멤버함수 뿐 아리나 멤버변수, 생성자, 소멸자를 통해 추가적인 운용이 가능하다.
멤버들을 순회하며 값을 더하는 방법을 만든다면,

struct accum  
{ 
 int sum; 
 accum() { sum=0;} 
 void operator() (int a) 
 { 
  sum+=a; 
 } 
}; 
... 
 
accum f 
f = for_each(vi.begin(), vi.end(), f); 
cout << "총합 : " << f.sum << endl; 

③ 함수객체는 타입이므로 템플릿의 인수로도 사용될 수 있다.

함수객체의 사용

for_each 함수는 순회 중에 할 일을 결정하기 위해 반드시 함수 객체를 부르도록 되어 있다.
find는 순회중의 반복자 값과 세 번째 인수로 지정한 값(val)을 == 연산자로 비교하여 정확하게 일치하는 요소를 찾아낸다. 그런데 때로는 ==로 정확한 일치를 검색하는 것이 아니라 사용자가 정의하는 방식으로 검색할 요소를 골라야 하는 경우도 있다. 이때 함수 객체로 요소를 직접 비교할 수 있는데 이런 함수는 보통 원래 함수의 이름 끝에 _if가 붙는다. find의 함수 객체 버전은 다음과 같다.
InIt find_if(InIt first, InIt last, UniPred F); 
세 번째 인수 F는 () 연산자를 오버로딩하는 함수 객체이며 요소값 하나를 인수로 전달받아 이 값이 원하는 조건이 맞는지 검사하여 bool형을 리턴한다. 찾는 조건에 맞으면 true를 리턴하고 아니면 false를 리턴할 것이다. 이처럼 bool을 리턴하는 함수 객체를 특별히 조건자(Predicate)라고 부르는데 요소가 지정 조건을 만족하는지를 검사하는 역할을 한다.

함수 객체로 만들 때와 함수 포인터를 쓸 때 인수의 형태가 조금 달라진다. 함수 포인터는 레퍼런스를 전달받는 것이 좋은데 왜냐하면 string같이 덩치가 큰 객체를 전달할 때 값으로 받으면 복사가 발생하며 이 비용이 무시할 수 없을 정도로 커질 수 있기 때문이다. int같은 단순 타입이라면 물론 값으로 전달하나 레퍼런스로 전달하나 전혀 차이가 없지만 말이다. 반면 함수 객체는 크기에 상관없이 값으로 전달받아야 하는데 왜냐하면 인라인으로 삽입되기 때문에 인수 전달 과정이 필요없기 때문이다.

find와 find_if 함수의 이름에 대해 고찰해 보자. 고정 값으로 검색하건 함수 객체를 쓰건 논리적으로 검색이라는 같은 연산을 하므로 두 함수의 이름을 통합하면 좋을 것 같은데 이름이 달라 불편하다. 이렇게 된 원인은 두 함수의 선언문이 사실상 동일하기 때문이다. 두 함수의 원형을 보자.

InIt find(InIt first, InIt last, const T& val); 
InIt find_if(InIt first, InIt last, UniPred F); 
만약,
find(vs.begin(),vs.end(),IsKim
?());
이 호출문의 세 번째 인수가 함수 객체이므로 함수 객체 버전을 호출해야 한다고 확신할 수 있을까? 만약 vs가 함수 객체의 벡터라면 그 중 IsKim?() 객체와 같은 요소를 검색하라는 명령이라고 우길 수도 있지 않겠는가? 물론 이건 억지에 불과하다. 이런 억지가 통하지 말아야 하기 때문에 find와 find_if는 하나로 통합되지 못하고 서로 다른 이름을 가질 수밖에 없는 것이다.

미리 정의된 함수 객체

함수 객체는 통상 () 연산자 하나만 정의하고 그나마도 동작이 간단해 길이가 아주 짧다. 이런 짧은 클래스도 직접 선언해서 쓰자면 번거로운데 그래서 STL은 자주 사용할만한 연산에 대해 미리 함수 객체를 정의하고 있다.

더하기에 대한 함수 객체의 예이다.

struct plus : public binary_function<T, T, T>  
{ 
     T operator()(const T& x, const T& y) const { return (x+y); } 
}; 
T가 아주 뚱뚱한 클래스일 수도 있으므로 값이 아닌 레퍼런스로 전달받고 피연산자를 상수로 취급한다는 정도 외에는 특별할 것도 없다.

함수 객체 연산
minus 두 인수의 차를 계산한다.
multiplies 두 인수의 곱을 계산한다.
divides 두 인수를 나눈 후 몫을 리턴한다.
modulus 두 인수를 나눈 후 나머지를 리턴한다.
negate 인수 하나를 전달받아 부호를 반대로 만든다.
equal_to 두 인수가 같은지 비교하여 결과를 bool타입으로 리턴한다.
not_equal_to 두 인수가 다른지 비교한다.
greater 첫 번째 인수가 두 번째 인수보다 큰지 조사한다.
less 첫 번째 인수가 두 번째 인수보다 작은지 조사한다.
greater_equal 첫 번째 인수가 두 번째 인수보다 크거나 같은지 조사한다.
less_equal 첫 번째 인수가 두 번째 인수보다 작거나 같은지 조사한다.
logical_and 두 인수의 논리곱(&&) 결과를 리턴한다.
logical_or 두 인수의 논리합( ) 결과를 리턴한다.
logical_not 인수 하나를 전달받아 논리부정(!)을 리턴한다.

함수객체의 종류

인수의 개수 bool이 아닌 리턴값 bool 리턴
없음 Gen
단항 UniOp? UniPred?
이항 BinOp? BinPred?

UniOp?는 인수 하나를 취하는 단항 함수 객체이며 BinPred?는 인수 둘을 취해 bool형을 리턴하는 조건자 함수 객체이다.

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

bool Pred(T &val) { } 

만약 알고리즘 함수가 요구하는 원형과 다른 함수 객체를 인수로 전달하면 어떻게 될까? 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의 본체에 맞는 함수 객체이기만 하면 되고 sort가 구현하는 코드를 제대로 실행할 수 있으면 되는 것이다. 알고리즘의 목적과 동작 과정을 잘 생각해 보면 아주 상식적이다. 비교 함수는 bool을 리턴하는게 당연하고 for_each의 인수는 하나일 수밖에 없다.

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

어댑터 (Adapter)

어댑터
컴포넌트 어댑터
스택
우선순위 큐
반복자 어댑터
역방향 반복자
함수객체 어댑터
부정자
바인더
함수포인터 어댑터

여기서는 함수객체 어댑터만 다루기로 한다.

합수객체 어댑터

어댑터를 적용할 수 있으려면 함수 객체는 어댑터가 요구하는 타입 정보를 제공해야 한다. 타입 정보를 제공하지 않는 함수 객체는 단독으로는 사용될 수 있지만 어댑터와 함께는 사용할 수 없다. 어댑터 적용을 위해 타입을 공개하는 함수 객체를 어댑터블 함수 객체라고 한다.

(똑같은 말 다시 말하자면...)
함수 객체의 기능을 조금이라도 변경하려면 어댑터를 적용할 수 있도록 만들어야 하는데 이런 함수 객체를 어댑터블(Adaptable) 함수 객체라고 한다. 기능을 변경하는 어댑터는 대상 함수 객체가 취하는 인수의 타입은 무엇인지, 리턴 타입은 무엇인지 등 함수 객체에 대한 충분한 정보를 얻을 수 있어야 한다. 만드는 방법은 아주 쉬운데 functional 헤더 파일에 정의되어 있는 다음 두 템플릿 클래스 중 하나를 상속받으면 된다.

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; 
}; 
  • 클래스는 주로 멤버 변수나 멤버 함수들로 구성되지만 타입이나 상수, 내부 클래스, 가상 함수 테이블 등의 다른 여러 가지 것들도 같이 캡슐화된다는 것을 잊지 말자.

인수의 개수에 따라 단항 함수 객체는 unary_function을 상속받고 이항 함수 객체는 binary_function을 상속받는다.
인수나 리턴 타입은 함수 객체와 관련이 있는 중요한 정보인데 이 정보들을 템플릿 인수로 전달받아 argument_type, result_type 등의 이름으로 획일화하여 타입 정의한다.

어차피 이 클래스들은 크기가 0이므로 상속을 받는다 하여 용량상의 불이익이 발생하는 것도 아니다. 그래서 어댑터를 적용할 계획이든 아니든 함수 객체 클래스를 정의할 때는 일단 상속을 받는 것이 좋다. plus, greater 등의 미리 제공되는 함수 객체들은 모두 이 클래스들로부터 상속받으므로 어댑터를 항상 적용할 수 있다.

사용예

#include <functional> 
 
struct print : public unary_function<int,void>  
{ 
     void operator()(int a) const  
     {  
          printf("%d\n",a);  
     } 
}; 

부정자
||부정자|| 적용대상||
||not1|| 단항 조건자 함수 객체(UniPred
?)||
not2 이항 조건자 함수 객체(BinPred?)

struct IsMulti3 : public unary_function<int,bool>  
{ 
     bool operator()(int a) const  
     { 
          return (a % 3 == 0); 
     } 
}; 
 
... 
 
it=find_if(it, vi.end(), IsMulti3()); 

부정자를 사용하면 이미 만들어져 있는 함수 객체를 조금만 변형하여 반대의 평가를 하도록 할 수 있다.

it=find_if(it, vi.end(), not1(IsMulti3())); 
  • not2() => not2 부정자는 이항 조건자의 평가 결과를 반대로 뒤집는다.

부정자 분석
template<class F> 
unary_negate<F> not1(const F& func) 
{ 
     return (unary_negate<F>(func)); 
} 

F 타입의 함수 객체 func를 인수로 전달받아 unary_negate<F> 클래스의 객체를 생성하되 생성자의 인수로 func가 전달된다. unary_negate는 다음과 같이 정의된 클래스 템플릿이며 인수로 함수 객체의 타입 F를 전달받는다.

template<class F> 
class unary_negate : public unary_function<typename F::argument_type, bool> 
{ 
protected: 
     F functor; 
public: 
     explicit unary_negate(const F& func) : functor(func) { } 
     bool operator()(const typename F::argument_type& left) const  
     { 
          return (!functor(left)); 
     } 
}; 

바인더
struct IsMulti : public binary_function<int,int, bool>  
{ 
     bool operator()(int a,int b) const  
     { 
          return (a % b == 0); 
     } 
}; 
앞의 IsMulti
?를 두개의 인수를 받게 만들어 일반성을 확보하였다.
하지만 이전과 같이 단항조건자를 요구하는 find_if와 같은 경우 사용할 수 없다.
바인더는 이항함수객체의 나머지 한 인수를 특정값으로 고정시켜 단항연산자로 반환한다.

bind1st(이항 객체, 고정값) 
bind2nd(이항 객체, 고정값) 
bind1st는 첫 번째 인수를 고정하며 bind2nd는 두 번째 인수를 고정한다.

it=find_if(it, vi.end(), bind2nd(IsMulti(),3)); 
앞의 경우 이렇게 바꿀 수 있다.

바인더 분석

template<class F, class T> 
binder2nd<F> bind2nd(const F& func, const T& right) 
{ 
     typename F::second_argument_type val(right); 
     return (binder2nd<F>(func, val)); 
} 
not1과 마찬가지로 직접 함수 객체를 호출하는 것이 아니라 함수 객체를 래핑하는 binder2nd 클래스의 객체를 생성하여 리턴한다. binder2nd 클래스는 다소 복잡하게 선언되어 있다.

template<class F>  
class binder2nd : public unary_function<typename F::first_argument_type, typename F::result_type> 
{ 
public: 
     typedef unary_function<typename F::first_argument_type, typename F::result_type> base; 
     typedef typename base::argument_type argument_type; 
     typedef typename base::result_type result_type; 
  
     binder2nd(const F& func, const typename F::second_argument_type& right) 
          : op(func), value(right) { } 
     result_type operator()(const argument_type& left) const { return (op(left, value)); } 
     result_type operator()(argument_type& left) const { return (op(left, value)); } 
protected: 
     F op; 
     typename F::second_argument_type value; 
}; 
binder2nd 클래스는 unary_function으로부터 상속받으므로 결국 단항 함수 객체이다. 내부에 이항 함수 객체 op와 고정된 두 번째 인수의 값 value를 멤버로 가지며 생성자에서 이 둘을 인수로 전달받아 초기화한다. 래핑한 이항 함수 객체를 호출할 수 있는 만반의 준비를 해 놓는 것이다.

() 연산자 함수는 op 함수 객체를 호출하되 첫 번째 인수 left는 자신이 전달받은 인수를 그대로 넘기고 두 번째 인수는 생성자에서 미리 받아 놓은 value를 넘긴다. 그래서 이 함수는 단항이며 호출할 때 left 인수 하나만 전달하면 된다. () 연산자는 첫 번째 인수에 대해 상수, 비상수 버전이 오버로딩되어 있다. bind1st도 비슷하게 분석되는데 binder1st 객체를 생성하며 binder1st는 첫 번째 인수를 미리 받아 놓았다가 호출할 때 고정된 인수를 전달할 것이다.

===== 함수포인터 어댑터 ======
함수 포인터 어댑터는 일반 함수의 번지인 함수 포인터를 함수 객체처럼 포장한다. 함수 포인터도 어차피 () 연산자로 호출할 수 있으므로 굳이 함수 객체로 만들지 않아도 알고리즘 함수와 함께 사용할 수 있다. 그러나 함수 포인터는 객체가 아니므로 어댑터는 적용할 수 없다.

template<class Arg, class Result> 
pointer_to_unary_function<Arg, Result> ptr_fun(Result (*pfunc)(Arg)) 
{ 
     return (pointer_to_unary_function<Arg, Result>(pfunc)); 
} 

template<class Arg,class Result> 
class pointer_to_unary_function : public unary_function<Arg, Result> 
{ 
public: 
     explicit pointer_to_unary_function(Result (*pfunc)(Arg)) : pFun(pfunc) { } 
     Result operator()(Arg left) const { return (pFun(left)); } 
 
protected: 
     Result (pFun*)(Arg); 
}; 

멤버함수 어댑터
ptr_fun은 일반 함수를 함수 객체로 만드는데 비해 mem_fun은 클래스의 멤버 함수를 함수 객체로 만든다. 멤버 함수는 반드시 this와 함께 호출되어야 한다는 점에서 함수 포인터와도 다른데 mem_fun은 이것을 가능하게 한다. 다음 예제를 보자.

template<class Result, class T> 
mem_fun_t<Result, T> mem_fun(Result (T::*Pm)()) 
{ 
     return (mem_fun_t<Result, T>(Pm)); 
} 

template<class Result,class T> 
class mem_fun_t : public unary_function<T *, Result> 
{ 
public: 
     explicit mem_fun_t(Result (T::*Pm)()) : Pmemfun(Pm) {} 
     Result operator()(T *pObj) const  
     { 
          return ((pObj->*Pmemfun)()); 
     } 
 
private: 
     Result (T::*Pmemfun)(); 
}; 

만약 컨테이너가 객체의 포인터가 아니라 객체 그 자체를 저장하고 있다면 이때는 mem_fun_ref를 사용한다. 위 예제를 다음과 같이 변형해도 동일하게 동작한다. 객체의 포인터를 동적으로 생성한 것이 아니므로 delete는 할 필요없다.

할당기(Allocator)

벡터, 리스트, 맵, 셋 등 STL 컨테이너의 저장 가능한 최대 요소 개수에는 제한이 없다. 요소가 삽입되면 필요할 때마다 메모리를 추가 할당하는데 이 과정은 컨테이너 내부에 완전 자동화 되어 있어 사용자가 신경쓰지 않아도 된다. 컨테이너들은 메모리 할당만을 전문적으로 관리하는 할당기 객체를 가지는데 보통 컨테이너의 템플릿 인수로 전달된다. 다음은 벡터 템플릿의 선언문이다.

template <class Type, class Allocator = allocator<Type> > class vector 
첫 번째 인수로 요소의 타입을 지정하며 두 번째 인수로 할당기를 지정하는데 할당기에는 디폴트가 있다. Type형에 대한 디폴트 할당기는 allocator<Type>이며 Type형의 자료를 저장하기 위한 메모리 관리를 담당한다. 메모리 할당 방식은 여러 가지가 있는데 디폴트 할당기는 C++의 new, delete 연산자를 사용한다. 만약 다른 방식으로 메모리를 직접 관리하고 싶다면 디폴트 할당기가 아닌 직접 만든 할당기를 사용할 수도 있다.

예를 들어 malloc/free를 사용할 수도 있고 COM 인터페이스를 쓸 수도 있고 운영체제의 가상 메모리를 직접 다룰 수도 있다. 할당기를 직접 만들어 쓰는 가장 실용적인 예는 할당, 해제가 아주 빈번하며 필요량이 많을 때 메모리를 미리 왕창 할당하여 메모리 풀을 만들어 놓고 이 메모리를 내부에서 관리하며 번갈아 쓰도록 할 때이다. 운영체제의 간섭에서 벗어나 메모리에 대한 완전한 통제권을 행사하고 싶을 때 이런 방법이 동원된다.

할당기라는 것이 과거 16비트 시절 세그먼트/오프셋 같은 복잡한 메모리 구조를 추상화하기 위한 목적으로 만들어진 것이라 지금은 사용자가 정의할 이유가 딱히 없는 셈이다.

설사 만들어 보고 싶다고 하더라도 기존의 STL 컴포넌트와 완벽하게 잘 조화되는 할당기를 만드는 것이 그다지 간단하지 않음을 직감적으로 느낄 수 있을 것이다. 특별한 이유가 없는 한 디폴트만 사용해도 충분하다. 여기서는 할당기도 STL의 구성 요소중 하나이고 꼭 원할 경우 교체 가능하다는 정도만 상식적으로 알아 두자.




글수정 | 글삭제
http://codesarang.com. mail to cpueblocpueblo.com