new로 생성한 포인터의 컨테이너를 사용할 때에는 컨테이너가 소멸되기 전에 delete하는 일을 잊지 말자

void doSomething()
{
vector<Widget*> vwp;
for( int i = 0; i < SOME_NUMBER; ++i )
{
vwp.push_back(new Widget);
}

}

위와 같은 코드를 보면 vwp 라는 컨테이너가 doSomething을 빠져나가면서 소멸될때 메모리가 샐것이라는것을 알수 있다. 이 문제는 스마트포인터를 사용하지 않는 이상 사용자 처리해 주어야 하는데 일반적으로 이렇게 짤수 있다.


struct DeleteObject{
template<typename T>
void operator()(const T* ptr) const
{
delete ptr;
}
};

void doSomething()
{
vector<Widget*> vwp;
for( int i = 0; i < SOME_NUMBER; ++i )
{
vwp.push_back(new Widget);
}

for_each(vwp.begin(), vwp.end() DeleteObject() );
}


하지만 위의 코드 역시 vwp 에 데이타를 삽입한 이후 예외상황이 발생하면 메모리는 새게 된다. 이 문제를 해결하는 방법은 스마트포인터를 사용하는 수밖엔 없다.
스마트포인터는 Boost 라이브러리의 shared_ptr 을 사용하면 좋다.

주의사항 : auto_ptr을 컨테이너에 넣으면 안된다

C++ 컴파일러의 어이없는 분석결과를 조심하자

ifstream dataFile(“ints.dat”);
list<int> data(istream_iterator<int>(dataFile),
istream_iterator<int>() );

위의 코드는 범위생성자를 사용해서 “ints.dat” 파일의 정수값을 읽어 list에 넣는다는 의미이다. 컴파일이 되기는 하지만 정상적으로 작동하지는 않을 것이다.
자세한 이유는 생략한다.
위의 코드가 정상적으로 작동하지 않는 이유는
class Widget{…};    // 기본생성자가 있다고 가정한다.
Widget w(); // 아무것도 받아들이지 않는고
// Widget을 반환하는 함수 w를 선언한다.

이것과 유사한 이유이다. 어쨋거나 위의 코드같은 문제가 일어나지 않기를 바란다면 익명객체선언을 istream_iterator에 쓰지말고 각 반복자를 변수로 할당하는 것이다.
ifstream dataFile(“ints.dat”);
istream_iterator<int> dataBegin(dataFile);
istream_iterator<int> dataEnd;
list<int> data(dataBegin, dataEnd);

단일요소를 단위로 동작하는 멤버함수보다 요소의 범위를 단위로 동작하는 멤버함수가 더 낫다.

두개의 백터 v1과 v2가 있다고 할때, v1의 내용을 v2의 뒤쪽 반과 같이 하는 가장 간단한 방법은





v1.assign(v2.begin()+v2.size()/2, v.end());

이다.
물론 루프를 사용할수도 있으며 copy 알고리즘과 back_insert 삽입연산자를 사용할수도 있다. 하지만 가능하다면 이경우는 vector에서 제공하는 멤버함수를 사용하는것이 좋다.

  • 범위멤버함수를 사용한 코드가 대개 짧다.
  • 범위멤버함수는 훨씬 명확하고 간결한 의미를 전달한다.
이다.
다른 예로 int 배열을 vector의 앞쪽에 복사할 경우
int data[numValues];
vector<int> v;

v.insert(v.begin(), data, data + numValues);
의 코드를 사용하게 되는데 이것을 copy 알고리즘을 이용해서 구현하면
copy(data, data + numValues, inserter(v, v.begin()));
같은 코드로 구성이 된다. 하지만 copy와 삽입연산자를 사용할 경우

  1. numValues 번에 해당하는 insert 함수의 호출하는 비용
  2. 앞쪽에다가 삽입함으로써 insert 호출시마다 vector를 뒤로 밀어버리는 비용
  3. 삽입시 컨테이너가 모자를 경우 생기는 메모리 할당 비용
같은 비용이 추가로 부담되게 된다. 왜냐하며 copy는 내부적으로 루프를 사용하는 것과 비슷한 방식으로 구현되어 vector의 insert를 여러번 호출하는것이나 마찬가지 이기 때문이다. 하지만 vector의 범위멤버인 insert를 호출함으로써 한번의 호출시 작업해야하는 범위를 파악하여 메모리 할당문제를 해결하고 순차적으로 각값을 넣음으로써 위의 세가지 비용문제를 해결할수 있다.

이런 문제는 vector 보다는 적지만 list 에서도 마찬가지 이다. list에 insert 를 호출시 삽입되는 위치의 앞과 뒤에 있는 노드의 서로를 연결시키는 포인터값을 수정해야 하며 이것 역시 추가적인 비용이 들어간다. 범위 insert를 호출한다면 위의 비용은 한번으로 줄어들 것이다.





정리

  • 범위 생성
    istream_iterator, istreambuf_interator 를 사용할 경우에는 주의
  • 범위 삽입 : 대부분의 컨테이너는 insert류의 함수를 제공한다. (연관컨테이너는 위치지정을 내부에서 비교함수를 따로 사용하기 때문에 위치지정은 없다.) push_back, push_front 나 back_insert, front_insert를 사용하는 루프나 copy 알고리즘이 있다면 insert 범위 멤버함수로 변경가능하다.
  • 범위 삭제 : erase 함수를 이영해서 삭제
  • 범위 대입 : 표준연속컨테이너에서 assign을 제공

size()의 결과를 0과 비교할 생각이라면 차라리 empty를 호출하자

STL에서 제공하는 컨테이너들은 각각의 라이브러리 마다 여러가지 방법으로 구현되어 있으며 당연히 컨테이너에서 실제로 추가되어 있는 객체의 개수를 얻는 방법도 여러가지 방법이다.
이것은 컨테이너의 size() 함수가 여러가지 방법으로 구현되어 있을수 있다는 의미이며 정확한 규칙에 의거한 속도를 보장하지는 않는 의미다.

예를 들어 list 의 객체의 개수를 간단히 얻는 방법은 처음부터 끝까지 노드를 따라가며 세어보는 방법이지만 list::size_type 을 맴버변수로 가지고 계속 값을 업데이트 해줌으로써 상수시간에 객체의 개수를 얻을 수도 있다.
하지만 list 의 splice 함수의 동작은 변경되는 개수가 몇개인지 세어보기 전에는 알수 없기때문에 값을 계속 업데이트 해주는 방법은 splice 함수에 불필요한 연산을 요구하게 된다. 어떤 방법에 의해서 size()와 splice 를 구현할지는 라이브러리제작자의 판단에 따르게 될것이다.

이런 이유로 라이브러리 간의 size()함수의 구현 방법이 다를수도 있으며 size() 함수가 선형적인 시간을 요구할수도 있으므로 해당 컨테이너가 단순히 비어있는지 아닌지 를 판단하기 위해서라면 간단히 상수시간에 결과를 리턴해주는 empty() 사용하는것이 좋다.

복사(copy)는 컨테이너 안의 객체에 맞게 비용은 최소화하며, 동적은 정확하게 하자

일반적으로 STL에 넣는 동작(push_back, insert) 이나 값을 얻어오는 동작은 모두 객체의 복사 를 통해서 이루어진다. 이 동작을 관리하는 연산자 두가지는 복사생성자, 대입연산자 이며 일반적으로

class Widget{
public:

Widget(const Widget& );
Widget& operator=(const Widget&);
}
형식으로 선언된다. 이 연산자들의 경우는 클래스에 대해 구현이 안되있을 경우에는 컴파일러가 자동적으로 생성해서 사용하게 된다. 이것과 관련된 내용으로 RuleOfThree 를 참조하는것이 좋을것 이다.
컨테이너의 동작이 객체를 복사하는 것이 기본이기 때문에 상속받아 구현한 클래스를 부모 클래스를 넣는 컨테이너에 넣으려하면 슬라이스 라는 현상이 발생하며 이것은 상속받은 클래스의 맴버함수나 가상함수를 호출하는데 문제를 일으키게 된다.
이것을 해결하는 방법은 포인터의 컨테이너를(물론 상위클래스의 포인터) 사용하는것 이지만 이 방법은 포인터의 객체 할당과 해제에 대해 신경을 써줘야 한다.

적재적소에 알맞은 컨테이너를 사용하자


  • 표준 STL 시퀀스 컨테이너 : vector. string, deque, list
  • 표준 STL 연관 컨테이너 : set, multiset, map multimap
  • 비표준 시퀀스 컨테이너 : slist(싱글링크드리스트), rope(대용량 string)
  • string 대산 사용되는 vector<char> : 가끔사용. 항목13참조.
  • STL 에속하지 않는 표준 컨테이너 : 배열, bitset, valarray, stack, queue, priorty_queue

  • 연속메모리 컨테이너 : list를 제외한 STL 시퀀스 컨테이너
  • 노드기반 컨테이너 : STL 연관 컨테이너, list





정리

  • 임의정렬을 사용할려면 연관컨테이너는 안된다.
  • 삽입이나 삭제시 컨테이너의 요소들이 밀려나는 일이 없어야 한다면 연속메모리 컨테이너는 안된다.
  • 배열과의 호환을 원하면 vector
  • 탐색속도가 중요하다면 해쉬컨테이너 -> 정렬된 백터 -> 연관 컨테이너 를 고려한다.
  • string 은 라이브러리 마다 참조카운터를 사용한 경우와 아닌 경우가 있으며 참조카운터 모드를 조절할수 있는 경우도 있다. 대용량 string인 rope 는 보통 참조카운팅을 이용한다.
  • 삽입과 삭제의 트랜잭션적 의미를 부여할려면 list
  • 반복자, 포인터, 참조자 무효화를 최소한 으로 하기 위해서는 노드기반 컨테이너.

삽질 – 002 :: transform, toupper 컴파일시 에러

gcc version 3.2.2 20030222 (Red Hat Linux 3.2.2-5) 에서 발생했음.

…쩝 STL 오류코드 보느라 눈깔빠지는 줄 알았는데..
transform( s1.begin(), s1.end(), s1.begin(), tolower );
이 코드의 경우 VC 에서는 문제없이 돌아가는데 gcc에서는 toupper 인자, 리턴형 문제때문인지

no matching function for call to `transform(
__gnu_cxx::__normal_iterator std::char_traits< char >, std::allocator< char > > >,
__gnu_cxx::__normal_iterator< char*, std::basic_string < char,
std::char_traits< char >, std::allocator< char > > >,
__gnu_cxx::__normal_iterator std::char_traits< char >, std::allocator< char > > >, < unknown type >)’

이딴 썩을 에러코드를 뺃으면서 컴파일이 안된다.
해결방법은
transform( s1.begin(), s1.end(), s1.begin(), (int(*)(int))tolower );
로 수정. 즉 toupper 의 인자와 리턴형을 명시적으로 int형으로 수정해준것이다. 소스코드를 봐봐야 알겠지만 위에서 언급한 컴파일러의 빌트인 펑션 touuper 변수와 리턴형이 아마도 char로 되어있는게 아닌가 싶다.
(…엇…char로 되어있어야 더 잘되야 되는거 아냐?!
…귀찮다…)

냠…어쨋든 메모메모.

ps. 저렇게 해놓는게 올바른 방법인지는 모르겠다.