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

요약하면 표준은 아니지만 거의 표준화된 인터페이스를 제공한다. 하지만 좀 다르다. -_-;
딩컴웨어와 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> 와 똑같이 동작

상등관계와 동등관계의 차이를 파악하자

상등관계는 operator= 에 의해서 비교되는 것으로 두 객체가 나타낸는 값이 동일한지를 판단한다.
동등관계는 operator< 에 의해서 비교되는 것으로 두 객체가 다른가를 판단한다.
위의 동등을 코드로 표현해 보면

if( !(w1<w2) && !(w2<w1) )
{
// 동등하다
}
else
{
// 동등하지 않다.
}
의 관계를 같는다고 할수 있다. 일반적으로 연관 컨테이너에서 사용되는 비교함수는 operator< 가 아니라 less(EffeciveSTL39참조)를 사용한다. 실제로 STL의 연관 컨테이너는 멤버함수를 통해 자신의 정렬용 술어구조를 내어주므로, 앞의 표현식은 다음과 같이 표현된다.(c는 연관 컨테이너 객체의 이름)
!c.key_comp()(x,y) && !c.key_comp()(y,x)
대소문자를 무시하는 연관컨테이너 작성법과 연관컨테이너에서 상등성과 동등성의 연산에 관한 문제는 책을 참조.

vector 보기를 돌같이 하자

vector<bool> 은 STL 컨테이너가 아니다
만약 bool 컨테이너가 필요하다면 deque<bool> 이나 bitset 을 사용하도록 하자.

vector<bool> 은 실제 bool을 저장하지 않고 한바이트를 나누어서 사용하는 8개의 bool 단위로 저장이 된다. 그런 이유때문에 참조자나 operator[] 연산자로 포인터를 얻는 동작이 정상적으로 작동할 수 없다고 한다.

복잡복잡

쓸데없이 남은 용량은 “바꿔치기(swap)묘수”를 써서 업애 버리자

어떤 vector가 있다고 할때 push_back, insert 나 범위 생성자로 vector의 할당된 용량이 늘어나 버렸다면 erase 등을 호출 하여 데이타를 삭제한다고 해도 이미 할당된 용량이 줄어들지는 않는다.
이런 경우

vector<Object>(v).swap(v);
위의 코드를 사용하면 할당된 용량이 데이타의 개수와 딱맡는 v로 만들수 있게 된다.
코드가 처리되는 방식은

  1. 복사생성자 에 의해 딱맞는 크기를 갖는 vector<Object> 임시객체가 v를 복사해서 생성
  2. swap() 에 의해서 백터의 데이타가 뒤바뀜
  3. 코드가 끝나면서 임시객체 소멸
  4. v는 타이트 해짐
으로 이해 하면 된다. (string에도 적용가능)
또한 swap는 컨테이너를 완전히 비우고 용량을 라이브러리 구현코드가 허용하는 최소치로 줄이는 데에도 쓸 수 있다.
vector<Object> v;
string s;

vector<Obect>().swap(v);
string().swap(s);

swap을 일반적으로 서로다른 컨테이너끼리 사용할 경우에 swap 일어난 후에도 원래의 컨테이너의 요소를 가르키는데 사용되던 모든 반복자나 포인터, 참조나는 다른 컨테이너로 옮겨간 이후에도 그대로 유지된다고 한다.(뭔소린지 -_-)

기존의 C API에 vector와 string을 넘기는 방법을 알아두자

vector<int> v 라는 객체가 있을때 int* 를 받는 함수에 넘기는 방법은 &v[0] 을 사용하면 된다.
vector는 연속된 메모리로 컨테이너를 관리하므로 저렇게 넘기면 배열을 사용하는 C언어 함수에서도 문제될것이 없다.(반복자 begin을 사용하는 경우를 생각해 볼수 있지만 반복자는 엄연히 반복자객체이지 배열의 시작포인터를 의미하지는 않는다. vector에서 이 둘이 같은것을 의미할 지라도 두개의 개념은 분리해서 사용하는것이 옳다.)

string 의 경우는 구현이 연속된 메모리로 규정되있지 않기때문에 위의 방법을 사용할수가 없다. 대신 std::string 의 맴버함수로 c_str() 이 있으며 이 함수가 NULL문자로 끝나는 문자열포인터를 리턴한다.





주의사항

  • string 의 c_str()은 const char*를 리턴하므로 문자열의 값을 바꿀수는 없다.
    -> 즉 바꿔야 한다면 바꾼결과를 리턴하는 방식을 사용해야 할것이다.

  • vector를 &v[0]의 형식으로 넘겼을때 배열값 자체를 수정하는것은 상관없지만 배열의 크기를 변경시키는 동작을 해서는 안된다.
  • vector 이외의 컨테이너를 C언어의 배열스타일로 다룰수는 없다. 이런 동작이 필요하다면 vector를 임시버퍼로 사용하는것이 좋다.