문자 단위의 입력에는 istreambuf_iterator의 사용도 적절하다.

어떤 텍스트 파일을 string 객체에 복사해야 한다고 할때

ifstream inputFile(“file.txt”);
string fileData((istream_iterator<char>(inputFile)),
istream_iterator<char>());
같은 코드를 사용할수 있다.

하지만 ifstream_iterator는 실제 스트림 일기를 수행할때에 operator<< 함수를 사용하며, 기본적으로 이 연산자 함수는 공백문자를 건너 뛴다. 즉 공백 문자를 포함하여 파일을 읽어들이고 싶다면 입력 스트림의 skipws 플래그를 설정해제 한다.
ifstream inputFile(“file.txt”);
inputFile.unsetf(ios::skipws);
string fileData((istream_iterator<char>(inputFile)),
istream_iterator<char>());

하지만 어떤 서식 처리가 없이 그냥 문자열을 읽어 들이는 경우 위의 코드는 계선의 여지가 있다. istream_iterator가 사용하는 operator<< 함수는 서식화 입력을 수행하기 때문에 그에 따른 오버헤드가 존재한다.

즉 서식화 입력이 필요가 없다면 istream_iterator 대신 istreambuf_iterator을 사용할수 있다. istream_iterator<char>가 operator<<를 통해 입력 스트림으로부터 문자를 읽어 들이는 반면 istreambuf_iterator<char> 객체는 스트림 자체의 버퍼를 직접 건드려서 다음 문자를 바로 읽어 들인다.
ifstream inputFile(“file.txt”);
string fileData((istreambuf_iterator<char>(inputFile)),
istreambuf_iterator<char>());

istreambuf_iterator는 간단하게 파일의 내용을 읽어들일뿐만 아니라 istream_iterator에 비교해서 볼때 속도도 빠르다.
ostreambuf_iterator 역시 비슷한 속성을 가지고 있다.(문자단위의 비서식화 출력 스트림 반복자)

reverse_iterator에 대응되는 기점 반복자(base iterator)를 사용하는 방법을 정확하게 이해하자

아래는 vector에 1~5까지의 5개의 int 데이타를 넣고 reverse_iterator를 써서 3을 가리키게 한후, iterator하나를 revserse_iterator의 기점으로 세팅하는 코드이다.


vector<int> v;
v.reserve(5); // 미리 할당 EffectiveSTL14 를 참고

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

// 3을 가리킨다.
vector<int>::reverse_iterator ri = find(v.rbegin(), v.rend(), 3);

// i가 ri의 기점과 같게 한다.
vector<int>::iterator i(ri.base());


위의 코드를 도식화 해서 표현하자면 이렇게 된다.

rend()  rbegin()
↓ ↓
┌─┬─┬─┬─┬─┐
│1│2│3│4│5│
└─┴─┴─┴─┴─┘
↑ ↑
begin() end()
(크아 그리느라 죽는줄 알았어!)

insert나 erase는 reverse_iterator를 인자로 받아들이지 않는 경우가 있기 때문에 위의 경우와 같이 reverse_iterator를 사용하다가 insert나 erase가 필요한 경우는 iterator로 변환을 해야 한다. 그 때 사용하는 메소드가 바로 base이고 위의 그림과 같은 대응되는 위치를 갖는다.


ri와 i, rbegin()과 end() 등의 위치가 미묘하게 다른것을 볼수 있는데
insert를 할 경우에는 저런 관계가 아주 정확하게 성립된다. STL의 세계 에서의 요소 삽입은 반복자가 가르키는 위치의 바로 에서 이루어 진다는 점을 생각할때 3을 가르키는 ri의 기점 반복자가 4를 가리키고 있으므로 ri의 base() 호출에 의한 i로 insert() 를 호출시 정확히 원하는 위치인 3과 4 사이에 데이타를 삽입하게 된다.


 


하지만 erase는 다르다.
위의 그림을 볼때 ri가 가르키는 데이타를 삭제해서 나타나는 원하는 결과는 3이 삭제되는것일 것이지만 base()함수를 이용해 기점 반복자 i를 얻어 erase()를 호출하면 4가 삭제되어 버릴것이다.
이런 erase관련 문제를 해결하기 위해서 reverse_iterator의 위치 값을 하나더 증가시킨뒤 base() 를 호출하는 방법을 생각할수 있다.
v.erase( (++ri).base() );       // ri가 가르키는 요소를 삭제한다.
즉 ri는 (++ri)에 의해서 2를 가르키게 되고 ri의 base() 결과인 i는 3을 가르키게 된다.

 


base()로 기점 반복자를 얻은뒤 그 기점 반복자의 위치를 변경하는 것도 방법이 될수 있지만 vector 와 string에서 문제가 발생한다. 자세한 내용은 책 참조

const_iterator를 iterator로 바꾸는 데에는 distance와 advance를 사용하자.

EffectiveSTL26 에서 설명한 것 처럼 iterator만 받아들이는 함수가 있어서 const_iterator를 iterator로 변환해야 하는 경우가 생길수 있는데 일반적으로 생각하는 캐스팅 연산으로는 변경을 할수가 없다. iterator 와 const_iterator 는 서로 상속 관계등이 존재하는것이 아닌 전혀 다른 클래스이기 때문이다. (vector 나 string은 iterator가 포인터이기 때문에 가능할수도 있다.)

const_iterator 에서 iterator의 변환은 아래의 코드와 같은식으로 진행된다.
typedef deque<int> IntDeque;
typedef IntDeque::iterator Iter;
typedef IntDeque::const_iterator ConstIter;

IntDeque d;

ConstIter ci;
// 생략
Iter i(d.begin());
advance(i, distance<ConstIter>(i,ci));

distance는 같은 컨테이너의 두 반복자 사이의 거리를 알려주며 advance는 첫번째 인자인 반복자를 두번째 인자의 지정된 거리만큼 전진 시킨다.
distance<ConstIter>(i,ci) 에 의해서 i가 자동은 const_iterator 로 변환되후(iterator는 const_iterator로 변환가능 EffectiveSTL26 참조) ci와의 거리를 구한다. advance는 그 거리만큼 i를 이동시켜 결과적으로 ci와 같은 위치를 가르키는 iterator를 얻게 된다.

이 변환 방법은 다 반복자 사이의 거리를 알아내는 것, 그리고 전진시키는 과정에서 각 반복자의 특성을 그래로 반영받는다. 즉 vector등은 상수시간에 처리가 될수 있지만 list등의 노드기만 컨테이너들은 선형시간이 걸릴수 있다.

const_iterator 나 reverse_iterator, const_reverse_itertor도 좋지만 역시 쓸만한 것은 iterator 이다

우선 반복자간의 변환가능성을 보면

  1. iterator는 모든 다른 반복자로 변환이 가능하다.
  2. reverse 반복자류는 해당하는 종류의 반복자로 base() 맴버함수를 통해 변환이 가능하다.
    (reverse_iterator -> iterator, const_reverse_iterator -> const_iterator )
  3. reverse_iterator는 const_reverse_iterator 로 변환이 가능한다.
하지만 STL에서는 반복자 사용시 아래와 같은 제약이 발생하는 경우가 있다.



  • 어떤 형태의 insert와 erase 멤버함수는 무조건 iterator 만을 넘겨야 한다.
  • const_iterator 를 iterator로 암시적으로 변환하는 방법은 없고 굳이 바꾸자면 EffectiveSTL27의 내용을 참조한다. 하지만 일반성도 떨어지고 효율에 대한 보장도 할수 없다.
  • reverse_iterator를 iterator로 변환할수 있으나, 변환후 약간의 조정이 필요하다. 자세한 내용은 EffectiveSTL28을 참고한다.
무척이나 미묘한 경우이며 별것 아니지만 해결하기 난감한 경우이다. 또한 const_iterator 와 iterator 를 비교하는 경우도 생길수 있는대 논리적으로 라면 반복자는 위치를 가르키는 것이므로 비교 연산에 문제가 없어야 하지만 특정 STL은 const_iterator 의 operator== 연산자를 잘못 구현하여 두 반복자 간의 비교가 컴파일이 안되는 경우도 발생할수 있다. 이런 여러가지 제한 상황을 생각할때 그냥 iterator 를 쓰는게 속이 편한 경우가 많다.

물론 const나 reverse는 STL에서 버그를 예방하고 유용하게 쓰이는 면이 있으므로 제한 상황을 잘알아두는 것이 좋다.

현재는 표준이 아니지만, 해쉬 컨테이너에 대해 충분히 대비해 두자.

요약하면 표준은 아니지만 거의 표준화된 인터페이스를 제공한다. 하지만 좀 다르다. -_-;
딩컴웨어와 SGI 버젼으로 구분해서 볼수 있다.

마지막 개인적인 생각으로 현재 자기가 쓰고 있는 버젼의 STL에서 구지 다른 버젼의 해쉬맵을 쓸 경우가 생긴다면 그냥 boost 라이브러리 컴파일해서 거기 있는거 쓰는것도 나쁘지 않을 것이라고 생각한다.

이후에 해쉬에 관한 표준이 나온다면 해당 내용에 추가 하자

map이나 multimap 등의 [] 연산자는 두가지 동작을 같는다. 첫번째는 해당 키값의 데이타를 갱신하는 것이고 두번째로 해당 키값의 데이타가 컨테이너에 존재하지 않는다면 추가를 하는 동작이다.

map이나 multimap 등의 [] 연산자는 두가지 동작을 같는다. 첫번째는 해당 키값의 데이타를 갱신하는 것이고 두번째로 해당 키값의 데이타가 컨테이너에 존재하지 않는다면 추가를 하는 동작이다.

문제는 추가를 하는 동작에서의 불필요한 오버해드이다.
이 오버헤드는 일반적으로 기본생성자가 아닌 특정 데이타 타입을 지원의 생성자를 지원하는 클래스에서 문제가 되는데
class Widget{
public:
Widget();
Widget(double weight);
Widget& operator=(double weight);
}
같은 식으로 선언된 클래스의 경우
map<int, Widget> m;
m[1] = 1.50;
위의 코드는 실질적으로 기본 생성자를 통해서 임시 Widget 변수를 생성하고 그걸 map에 추가하고 그 후에 키값이 1인 항목을 찾아 수정하는 과정을 거치게 된다.
typedef map<int, Widget> IntWidgetMap;
pair<IntWidgetMap::iterator, bool> result = m.insert(IntWidgetMap::value_type(1, Widget()));
result.first->second = 1.50;
Widget 객체를 임시로 만드는데 필요한 기본 생성자 함수, 이것을 없에는 대 필요한 소멸자 함수, 그리고 Widget의 대입 연산자 함수가 추가 적으로 호출된다.

[] 연산으로 삽입이 될경우 이런 오버헤드를 얻게 될수 있지만 명시적으로 insert를 사용한다면 해당 오버해드를 줄일수 있다.
m.insert(IntWidgetMap::value_type(1,1.50));

insert 함수를 이용한 갱신을 할경우는 위와는 반대의 일이 일어난다. insert함수를 위한 추가적인 생성자가 호출되고 이후 대입연산이 이루어 진다.
// [] 연산자를 이용한 갱신
m[k] = v;

// insert를 이용한 갱신
m.insert(IntWidgetMap::value_type(k,v)).first->second = v;


결론을 내리자면 map에서 insert와 [] 적절한 위치에 사용을 해야 하며 만약 해당 키값의 데이타가 존재하는지 모를 경우에는 lower_bound 등의 검색함수를 이용하여 해당 데이타가 존재하지 않는다면 insert를 호출, 존재한다면 갱신하는 루틴을 작성하도록 한다.
(lower_bound 의 리턴값은 약간 특이. EffectiveSTL45 참조)

연관 컨테이너 대신에 정렬된 vector를 쓰는 것이 좋을 때가 있다

빠른 데이타 검색을 지원하는 자료구조가 필요할 경우 많은 경우 연관 컨테이너를 사용한다. (물론 비표준의 해쉬컨테이너가 더 빠를것이다.)
하지만 vector를 정렬되어 있다고 가정할 경우 대부분의 검색 속도에서 vector가 더욱 빠른 검색 속도를 보여준다. 연관 컨테이너는 전형적으로 이진 탐색 트리로 구현되어 있다. 이 트리는 테이터 노드의 삽입, 삭제, 탐색이 임의대로 아무 때나 이루어지는 상황에 적합하도록 구현된 자료구조 이다. 하지만 많은 상황에서 그런 복잡한 구조를 가지지는 않을 것이다.



  1. 데이터 셋업, 생성
  2. 데이터 탐색
  3. 데이터 재구성
의 구조로 루틴이 진행될 경우 연관 컨테이너로 구성하기 보다 vector로 구성하고 1번과 2번사이에 정렬을 해주는 루틴을 추가 한다면 더욱 성능을 높힐수 있다.

정렬된 vector가 연관 컨테이너보다 유리한 점은 간단하게 2가지 정도를 생각할수 있다.

  1. 하나의 데이터당 사용하는 메모리의 크기
    연관 컨테이너의 경우 노드가 포인터로 연결된 구조를 사용하기 때문에 포인터 만큼의 메모리를 더 사용한다. (최소 포인터 3개)
  2. 메모리 참조위치의 근접성
    vector 같은 연속 메모리 구조를 취하는 데이터구조는 논리적으로 인접해 있는 데이터가 물리적으로도 인접해 있게 되기 때문에 메모리 접근에 지역성에 따른 속도 계산을 생각할수 있다.
물론 해당 vector는 검색시 정렬되어 있는 상태여야 한다는 단점이 있으며 이후 push_back 등의 재할당에 의한 성능저하가 일어날수 있다. 여러가지 성능을 잘 계산하여 검색에 대한 오버헤드가 중요하다면 정렬된 vector를 사용하는것이 좋을것이다.

이후 자세한 내용은 생략한다. pair 객체를 넣고 비교함수를 어쩌구 저쩌구 하는 방법은 책을 참조하던지 나중에 적어 넣던지~

set과 multiset에 저장된 데이터 요소에 대해 키(key)를 바꾸는 일은 피하자

우선 알아둬야 할 사항은 set과 multiset의 경우 하나의 값을 넣는대 그 값이 순서를 유지하는 역할인 Key값이 되므로 변경해서는 안된다. 하지만 구조체나 클래스 일경우 그 일부분만 Key가 되고 나머지 부분은 그저 데이타일 뿐이므로 변경을 해도 set의 순서가 엉망이 되는 경우는 없다. 그렇게 때문에 일반적으로 set의 데이타타입은 변경이 가능하도록 const 형이 아니도록 되어있다.

map과 multimap의 경우는 두개의 테이타타입을 넣고 그중 Key는 const 타입이 된다.

하지만 위의 사항에서 문제점은 일부 STL의 경우 set의 데이타타입이 const가 아니라고 해도 컨테이너에 저장된 데이타요소의 변경을 막도록 구현되 있는 경우가 있다는 것이다.
set<T>::iterator 에 대한 operator* 연산자가 const T& 를 반환
(모호한 표준안에 의해 다른 해석을 한 STL구현방식 이라고 설명하고 있음)

이런 경우에 대비하고 이식성을 고려한다면 set, multiset의 요소값을 변경하지 못한다고 가정하는 것이 옳다. 하지만 약간의 편법이 cast를 이용해서 이 방법을 빗겨나갈수 있다.
// Object는 set에서 Key역할을 하는 ID와 기타 데이타로 구성
set<Object>::iterator i = se.find( objectID );

// 밑의 코드는 일부 STL에서는 적용되지 않는다.
if( i != se.end() ) {
i->SetName(“yoway”); // Key가 아닌 데이타는 변경하고 싶다.
}

// 아래처럼 캐스팅을 사용한다.
if( i != se.end() ) {
const_cast<Object&>(*i).SetName(“yoway”);
}

// 아래처럼 하면 안된다.
if( i != se.end() ) {
static_cast<Object>(*i).SetName(“yoway”);
}

첫번째 if문은 특정 STL에서는 안될 가능성이 있다. 두번째 if문을 사용하면 해결이 가능하지만 세번째 if문은 캐스팅을 한 결과가 임시객체이기 때문에 임시객체를 변경해도 부질없다.(그렇기 때문에 참조자로 캐스팅)

캐스팅을 사용하고 싶지 않다면







  1. 변경하고자 하는 컨테이너 요소의 위치를 찾는다.(EffectiveSTL45 참조)
  2. 변경하고자 하는 요소의 복사본을 만든다. (map의 Key를 변경하는 경우는 복사본의 Key의 경우는 const로 하지 않는다.)
  3. 컨테이너에서 해당 요소를 삭제한다. (EffectiveSTL09 참조)
  4. 만들어둔 복사본의 정보를 원하는대로 바꾼다.
  5. 복사본을 컨테이너에 삽입한다. 만약 새로 삽입되는 복사본의 위치가 제가된 요소의 위치와 같거나 그 옆(즉 Key같이 변경되지 않았을 경우)이면 윗 단계에서 요소의 위치를 파악했던 반복자를 이용해서 삽입함으로써 상수시간에 삽입이 가능하다.



// Object는 set에서 Key역할을 하는 ID와 기타 데이타로 구성
set<Object>::iterator i = se.find( objectID );
if( i != se.end() ) {
Object o(*i);
se.erase(i++);

o.SetName(“yoway”);
se.insert(i,o);
}

연관컨테이너용 비교 함수는 같은 값에 대해 false를 반환해야 한다.

set<int, less_equal<int> 라는 연관 컨테이너를 선언하고 사용할때 똑같은 정수형을 두개를 넣개 되면 두번째 값을 넣을대 해당 값이 있는지 검색하는 과정을 거치게 된다. 이 과정에서 일반적인 set 이라면





!(A < B ) && !(B < A)

를 사용하지만 less_equal 을 비교함수자로 사용하기로 했기 때문에 실제 수식은





!(A <= B ) && !(B <= A)

위에서 가정한 것과 같이 A 와 B가 같은 값이라고 하면 아래의 비교식은 결과적으로 false를 나타내게 되고 A와 B는 동등하지 않다 라는 결과를 얻게 된다. 그리고 결과적으로 set은 중복되는 값을 갖게 된다.
같은 값에 대해서 true를 반환하는 비교함수는 모두 동일한 결과를 얻게 된다.

위의 문제는 동등한 값의 중복을 허용하는 multiset과 multimap에서도 동일하게 발생하는데 동등한 값의 범위를 반복자 두개로 리턴해 주는 equal_range 의 경우 위에서 같은 값이라고 가정한 A와 B가 less_equal 비교연산자에 의해 동일하지 않은 값 으로 판단되기 때문에 equal_range의 리턴값의 같은 범위에 들어가지 않는 결과가 발생하게 된다.

포인터를 저장하는 연관컨테이너에 대해서는 적합한 비교(비교 함수자) 타입을 정해주자

set<string*> 같은 컨테이너를 사용할때 비교자를 설장해 주지 않는다면 set은 포인터의 크기를 사용한 비교를 하게 된다. 즉 포인터를 넣을때 포인터값으로 정렬되는 것을 원치 않는다면 그 포인터형을 받아서 적절히 비교 연산을 할수있는 비교 함수자가 필요한 것이다.

STL의 템플릿 매개변수는 타입이어야 하므로
struct StringPtrLess :
public binary_function<const string*,
const string*,
bool>
{
bool operator()(const string* ps1, const string* ps2 ) const
{
return *ps1 < *ps2;
}
};

typedef set<string*, StringPtrLess> StringPtrLess;
StringPtrSet ssp;
….

코드의 위 같이 함수자를 만들어준다. (binary_function 같은 기본 클래스는 EffectiveSTL40을 참조)
아랫 부분은 set을 선언하는 간단한 예시이다. 이후는 그냥 포인터를 넣어주면 된다.

포인터의 연관 컨테이너를 만드는 일과 컨테이너의 비교 타입(비교함수가 아님)을 지정하는 일은 실과 바늘의 관계라고 생각해 두는것이 좋다. 일반적으로 포인터를 역참조 하여 그 포인터가 가르키는 객체의 값으로 비교를 하게 될 것이니 코드 작성시 이런 경우가 잦을 때에는 비교함수자의 템플릿을 준비해 두는것이 좋을 것이다.
string DerefenceLess{
template<typename PtrType>
bool operator()(PtrType pT1, PtrType pT2) const
{
return *pT1 < *pT2;
}
};

set<string*, DerefenceLess> ssp; // set<string*, StringPtrLess> 와 똑같이 동작