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

상등관계는 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를 임시버퍼로 사용하는것이 좋다.

잊지말자 string 은 여러 가지 방식으로 구현되어 있다는 사실을…

string이 기본적으로 가지는 정보


  • 문자열의 크기 (size)
  • 문자를 담아두는 메모리의 용량(capacity)
    용량은 실제 문자열의 개수와 다를수 있다.

  • 문자열의 값(value)
선택적인 정보

  • 할당자의 사본
참조 카운팅을 사용하는 string의 추가적인 정보

  • 문자열 값에 대한 참조 카운트(reference count)
이런 정보들은 각각의 STL에 따라 여러가지 방법은로 저장, 관리 된다.
예를 들어 string 객체가 위의 정보를 클래스의 인스턴스로 가지고 있는 방법이 있을수도 있고 string 객체는 실제로는 이런 정보를 가지고 있는 객체를 나타내는 포인터일수도 있다.( 이런 경우 string 객체를 생성할때 동적할당이 2번일어 날수 있다.)
또한 어떤 STL의 string 의 경우는 15자 이하는 내부버퍼에 담아두고 그 이상되는 문자열의 경우 동적할당을 하여 값을 저장하며 참조카운터등을 전혀 사용하지 않는다.





정리

  • string 문자열의 값은 참조 카운팅이 될 수도, 되지 않을 수도 있습니다. 기본적으로 많은 라이브러리에서 참조 카운팅을 사용하고 있습니다만, 이 기능의 동작을 막는 방법(대개 전처리자 매크로를 통해)도 제공하고 있습니다. 참조 카운팅 기능을 막거나 사용해야 하는 경우에 대해서는 항목 13에서 이야기해 두었습니다. 예를 들어, 동일한 문자열이 아주 자주 복사되고 어떤 애플리케이션 자체에서 문자열 복사를 많이 하지 않을 때에만 참조 카운팅이 효과적입니다.
  • string 객체 자체의 크기는 포인터 크기의 한 배에서 일곱 배까지 다양합니다.
  • 문자열을 새로 생성할 때 필요한 메모리 할당의 회수는 0번, 1번 또는 2번이 될 수도 있습니다.
  • 문자 버퍼를 위해 할당하는 메모리의 최소량에 대한 정책은 구현된 라이브러리마다 천만별 입니다.

reserve는 필요 없이 메모리가 재할당되는 것을 막아 주다.

vector와 string에 있어서의 메모리 증가는 재할당이란 과정을 거쳐서 이루어 진다.(현재 사이즈의 증가가 아닌 컨테이너에서 할당해놓은 메모리가 부족해 질때)


  1. 컨테이너의 현재 용량의 몇배가 되는 메모리 블록을 새로 할당 (일반적으로 2배)
  2. 컨테이너가 가지고 있었던 메모리에 저장된 모든 요소 데이타(객체)를 새 메모리에 복사
  3. 원래 메모리에 저장된 모든 객체를 소멸
  4. 원래의 메모리를 해제
라는 과정을 거친다.
이런 오버헤드를 줄이기 위해서 필요한 만큼의 메모리를 미리할당하고 데이타를 삽입한뒤 관리하는 방식을 사용할수 있다. 이때 사용하는것이 reserve 이다.
예를 들어
vector<int> v;
for(int i=0; i<1000; i++) v.push_back(i);
라는 코드는 루프를 도는 동안 대략 10번 정도의 메모리 재할당이 일어나게 된다.(메모리크기를 2배씩 잡을때) 하지만 reserve를 사용하면 이런 불필요한 메모리 재할당을 막을수 있다.
vector<int> v;
v.reserve(1000);
for( int i=0; i<1000; i++ ) v.push_back(i);
위의 코드는 루프를 도는 동안 메모리 할당이 한번도 일어나지 않게 된다.
그리고 size와 capacity의 관계를 잘 생각해보면 요소 삽입시 vector나 string에서 재할당이 일어나는 시기를 예측할수 있다.
string s;

if( s.size() < s.capacity() ) s.push_back(‘X’);

정리하면 reserve를 이용해서 불필요한 메모리 재할당을 피하는 방법은 두가지 정도 이다.

  • 컨테이너에 저장될 요소의 개수를 미리 파악해서 정확한 reserve 호출
  • 컨테이너에 저장될 최대 요소의 개수를 할당하고 요소 삽입후 남는 메모리를 삭제 (EffectiveSTL17 을 참고)





참고

  • size() : 현재 컨테이너에 들어있는 요소의 개수
  • capacity() : 현재 컨테이너에 재할당없이 추가할수 있는 요소의 개수
  • resize(size_t n) : 컨테이너의 요소의 개수를 n으로 변경. 만약 현재 컨테이너의 개수가 n보다 크다면 n이후의 요소들은 삭제, 크다면 n크기로 재할당하고 뒤에 남는 공간은 요소 객체의 기본생성자로 생성해 채워 놓는다.
  • reserve(size_t n) : 컨테이너의 용량을 최소 n으로 맞춘다. 현재의 capacity()가 더 작다면 n의 크기로 재할당을 하며, 크다면 아무것도 하지 않는다. (이미 할당된 메모리의 공간을 줄이는 일이 일어날수도 있지만 삽입된 데이타를 줄이는 일은 없다.)

STL 컨테이너가 쓰레드 안정성에 대한 기대는 현실에 맞추어 가자

SGI에서 제정한 STL 다중 쓰레딩 지원 항목







  • 여러 쓰레드에서 읽는 것은 안전하다.(Multiple readers are safe)
    하나 이상의 쓰레드가 하나의 컨테이너의 내용을 동시에 읽는 경우가 있는데, 제대로 동작합니다. 당연한 이야기지만 읽기 도중에 쓰기 종작이 수행되면 안됩니다.
  • 여거 쓰레드에서 다른 컨테이너에 쓰는 것은 안전하다.(Multiple writers to different containers are safe)
    하나 이상의 쓰레드가 다른 컨테이너에 동시에 쓸수 있다.


STL에서는 위에 써있는 당연한것 이외에는 다중 쓰레딩 환경에 대해 전혀 지원받을수 없으니 자체적으로 해결하는 것이 좋다.
클라이언트 코드레벨에서 뮤텍스나 크리티컬섹션을 처리해 주는 수밖에 없다.

Lock 클래스의 구현을 생각해 보는것도 좋다 Lock 클래스 같은 경우는 생성자에서 객체(일반적으로 컨테이너)를 받아 다른 쓰레드에서 사용 못하도록 처리하고 소멸자에서 잠근걸 푸는 형식이 일반적이다.
vector<int> v;

{
Lock(v); // 잠근다.
……
} // 자동으로 소멸.
위의 코드 같은 경우 예외 상황이 발생해도 소멸자의 호출은 보장하므로 v가 영원히 다른 쓰레드에서 사용못하게 되는 일은 없다.

데이타를 삭제할 때에도 조심스럽게 선택할것이 많다.

컨테이너네서 특정한 값을 가진 객체를 모두 없애려면







  • 컨테이너가 vector, string 혹은 deque 이면, erase-remove 합성문을 씁니다.
  • 컨테이너가 list 이면, list::remove 를 씁니다.
  • 컨테이너가 표준 연관 컨테이너이면, erase 멤버함수를 씁니다.


컨테이너에서 특정한 술어 구문을 만족하는 객체를 모두 없애려면







  • 컨테이너가 vector, string 혹은 deque 이면, erase-remove_if 합성문을 씁니다.
  • 컨테이너가 list이면, list::remove_if를 씁니다.
  • 컨테이너가 표준 연관 컨테이너이면, remove_copy_if와 swap을 쓰든지, 컨테이너 내부를 도는 루프에서 erase를 호출하면서 erase에 넘기는 반복자를 후위증가연산자로 증가시킵니다.


루프 안에서 무엇인가를 하려면(객체 삭제도 포함해서):







  • 컨테이너가 표준 시퀀스 컨테이너이면, 컨테이너 요소를 하나씩 사용하는 루프를 작성합니다. 이때 erase를 호출할 때마다 그 함수의 반환값으로 반복자를 업데이타하는 일을 꼭 해야 합니다.
  • 컨테이너가 표준 연관 컨테이너이면, 역시 컨테이너 요소를 하나씩 사용하는 루프를 작성합니다. 이때 erase를 호출하면서 erase에 넘기는 반복자를 후위 증가 연산자로 증가 시킵니다.

auto_ptr의 컨테이너는 절대로 만들지 말자

auto_ptr 의 복사의 의미 auto_ptr 하나를 복사(copy)하면, auto_ptr이 가르키는 객체의 소유권은 복사하는 auto_ptr로 옮겨지고 복사되는 auto_ptr은 NULL로 세팅된다. auto_ptr을 복사하는 것은 그 포인터의 값을 바꾸는것 라고 말할수 있다.

auto_ptr<Widget> pw1(new Widget);        // pw1은 생선된 Widget을 가르킨다.
auto_ptr<Widget> pw2(pw1); // pw2는 pw1의 Widget을 가르키고
// pw1는 NULL로 세팅된다.
pw1 = pw2; // pw1는 다시 Widget을 가르키고 pw2는 NULL

여기서 생기는 문제는 STL의 컨테이너에서 값 넣는것 값을 꺼내오는것 대부분의 동작이 복사로 이루어진다는것이다. 간단한 예로 사용자가 vector에서 값을 꺼내올때

auto_ptr<Object> ap = Vec[0];

이라는 코드를 쓸때 ap에는 원하는 값이 들어가지만 Vec[0]에는 NULL로 세팅되어 버리는것이다. STL 의 기본적인 동작뿐만 아니라 알고리즘, 함수 등 모든 부분에서 복사를 사용하므로 auto_ptr을 컨테이너에 넣을 경우 원하지 않는 동작을 하게 될것이다.

auto_ptr을 컨테이너에 넣는 행위는 표준화위원회에서도 금지 이니 그냥 신경 안쓰는게 좋다.