- STL을 구성하는 핵심 요소에는 여러 가지가 있다.(Iterator, Generic Algorithm, Container 등등). 역시 가장 핵심적이고, 조금만 알아도 쓸수 있고, 편하게 쓸수 있는 것은 Container다. Container는 Vector, List, Deque 과 같이 데이터를 담는 그릇과 같은 Object라고 보면 된다.
- STL의 Container들의 장점이라고 한다면, 역시 유연성, 메모리 관리 알아서 하기, 자신이 알아서 늘었다 즐었다 하기 등등이 있겠다. 또한 인터페이스가 이거나 저거나 비슷비슷해서 하나만 공부하면, 쉽게 다른것도 쓸수 있다는 것도 또 하나의 장점이 될수 있겠다.
- 큰 1장 Containers에서는 상황에 맞는 적절한 Container 고르는 법, 효율성 극대화 하기 등등을 다룬다.
Contents
- 1. Item1. Choose your containers with care.
- 2. Item2. Beware the illusion of container-independant code.
- 3. Item3. Make copying cheap and correct for objects in containers.
- 4. Item4. Call empty instead of checking size() against zero.
- 5. Item5. Prefer range member functions to their single-element counterparts.
- 6. Item6. Be alery for c++'s most vexing parse
- 7. Item7. When using containers of newed pointers, remember to delete the pointers before the container is destroyed
- 8. Item8. Never create containers of auto_ptrs.
- 9. Item9. Choose carefully among erasing options.
- 10. Item10. Be aware of allocator conventions and restrictions.
- 11. Item11. Understand the legitimte uses of custom allocators.
- 12. Item12. Have realistic expectations about the thread safety of STL containers.
1.1. STL이 지원하는 Containers ¶
- Sequence Containers - vector, deque, list, string( vector<char> ) 등등
- Associative Containers - set, multiset, map, multimap 등등
- 그 외에 Non Standard라고 써있는게 있긴 한데, 잘 안쓰는 것 같다. 혹시라도 나중에 중요성이 부각되면 다시 봐야겠다. 딴게 급하니 일단.. AfterCheck
- vector가 좋다고 써있다. 잘 쓰자. 가끔가다 시간,저장공간 등등에 있어서 Associative Containers를 압도할때도 있다고 한다.
- vector 는 Sequence Container 니까 보통 Associative Container 들(주로 Map)보다 메모리낭비가 덜함. 그대신 하나 단위 검색을 할때에는 Associative Container 들이 더 빠른 경우가 있음. (예를 들어 전화번호들을 저장한 container 에서 024878113 을 찾는다면.? map 의 경우는 바로 해쉬함수를 이용, 한큐에 찾지만, Sequence Container 들의 경우 처음부터 순차적으로 좌악 검색하겠지.) --1002
- vector 는 Sequence Container 니까 보통 Associative Container 들(주로 Map)보다 메모리낭비가 덜함. 그대신 하나 단위 검색을 할때에는 Associative Container 들이 더 빠른 경우가 있음. (예를 들어 전화번호들을 저장한 container 에서 024878113 을 찾는다면.? map 의 경우는 바로 해쉬함수를 이용, 한큐에 찾지만, Sequence Container 들의 경우 처음부터 순차적으로 좌악 검색하겠지.) --1002
1.2. vector, deque, list 고르는 팁 ¶
- vector는 무난하게 쓰고 싶을때
- list는 중간에서 삽입, 삭제가 자주 일어날때
- deque는 종단점(시작과 끝)에서 삽입, 삭제가 자주 일어날때
1.3. Contiguous-memory Containers & Node-based Containers ¶
- 전자는 배열을 기반으로 하는 Container(즉, 동적 메모리 할당을 할때 할당된 메모리들이 연속적이 된다), 후자는 노드를 기반으로 하는 Container(노드의 경우는 반드시 연속적인 메모리가 아닐 수 있다.)다. 노드가 뭔지는 링크드 리스트를 짜보면 알게 된다.
- 전자에는 vector, string, deque 등이 있다. 뭔가 또 복잡한 말이 있긴 한데 그냥 넘어가자. 대충 insert, delete가 일어나면 기존의 원소가 이동한다는 말 같다.
- 후자에는 list 등이 있다. 노드는 그냥 포인터만 바꿔 주면 insert, delete가 자유자재로 된다는거 다 알것이다.
1.4. STL Container 고르는 팁 ¶
- 양끝에서 insert, delete 하려면? Associative Containers는 쓰면 안된다.
- Random Access Iterator(임의 접근 반복자)가 필요하다면, vector, deque, string 써야 한다. (rope란것도 있네), Bidirectional Iterator(양방향 반복자)가 필요하다면, slist는 쓰면 안된다.(당연하겠지) Hashed Containers 또 쓰지 말랜다.
- Insert, Delete 할때 원소의 인덱스 위치가 변하면 안된다? 그러면 Contiguous-memory Containers는 쓰면 안된다. (해당 원소의 인덱스 위치가 변한다.)
- Search 속도가 중요하다면 Hashed Containers, Sorted Vector, Associative Containers를 쓴다. (바로 인덱스로 접근, 상수 검색속도)
- Insert, Delete를 효율적으로 쓰려면, Node Based Containers를 쓰자.
- Iterator, Pointer, Reference 갱신을 최소화 하려면 Node Based Containers를 쓴다.
2.1. 완벽한 Container 독립적인 코드 작성은 불가능? ¶
- Standard Contiguois-memory Container들은 임의접근 연산자 []을 지원하지만, 다른건 지원하지 않는다.
- Standard Node-based Container들은 양방향 반복자(bidirectional Iterator)를 지원한다.
- 결국 이 코드에서 이 컨테이너에서 작동하는 함수를 썼는데, 컨테이너를 같은 계열의 것으로 바꾸면 좋겠지만, 성향이 다른걸로 바꾸면--; 많이 고쳐야 할것이다.
- 각각의 컨테이너는 장점과 단점을 가지고 있다. 고르는 능력을 기르자.
STL Tutorial and Refereince 를 보면, 일부러 해당 Container 에게 최적화된 메소드들만 지원한다고 써있었던 기억. 예를 든다면, Vector 의 경우 push_front 같은 것이 없다. (만일 vector에 push_front 를 한다면? push_front 될때마다 매번 기존의 원소들 위치가 바뀐다.) --1002
2.2. 그나마 고치기 쉽게 하는 팁들 ¶
- typedef을 쓰자.
~cpp vector<Type>::itarator // typedef vector<Type>::iterator VTIT 이런식으로 바꿀수 있다. 앞으로는 저렇게 길게 쓰지 않고도 VIIT 이렇게 쓸수 있다.
- Encapsulization을 잘 하자. 바뀌는 것에 대한 충격이 줄어들 것이다.
3.1. STL way ¶
- 컨테이너에 넣을 때나 뺄 때, 복사를 한다.
- 컨테이너에 Object를 넣을때에는, 그 Object의 복사 생성자(const reference)와, 대입 연산자를 재정의(const reference) 해주자. 안그러면 복사로 인한 엄청난 낭비를 보게 될것이다.
3.2. 상속받은 객체를 넣을때의 문제점 ¶
- Slicing에러
~cpp vector<Widget> vw; class SpecialWidget : public Widget ... SpecialWidget sw; vw.push_back(sw) // 어떻게 될까. 복사되면서 SpecialWidget만의 특수성은 제거되고, Widget의 특성만 남게 된다.
- 해결책으론 가상함수 등등이 있다. 하지만 더 좋은 방법이 있다.
3.3. 해결책 ¶
- 컨테이너에 값을 넣지 말고, 포인터를 넣는 것이다.
~cpp vector<Widget*> vw; vw.push_back(new SpecialWidget); // 잘된다.
- 하지만 역시 내가 delete해줘야 한다는 단점이 있다. 대안으로 smart pointers라는게 있다.(이게 뭘까)
MoreEffectiveC++/Techniques1of3 의 Item 28 을 봐라. 영문을 보는것이 더 좋을것이다. MoreEffectiveC++ 의 전자책과 아날로그 모두다 소장하고 있으니 필요하면 말하고, 나도 이 책 정리 할려고 했는데, 참여하도록 노력하마 --상민
4. Item4. Call empty instead of checking size() against zero. ¶
- 컨테이너에 하나도 안들었다는것을 가정할때 size() == 0 이것보다 empty() 가 더 좋다고 한다.
- empty()는 상수 시간에 수행이 가능하고, size()는 선형 시간에 수행이 가능하다.
- 고로 empty() 쓰자.
5. Item5. Prefer range member functions to their single-element counterparts. ¶
- 범위 가지고 노는 메소드가 하나씩 가지고 노는 메소드보다 우월하다. 수행시간의 차이가 있는듯하다.
5.1. a 컨테이너에 있는 내용의 뒷부분의 절반을 b라는 벡터 컨테이너에 넣고 싶다면, 어떻게 해야할까? ¶
- STL에 익숙하지 않다면 아마도 다음과 같이 할 것이다.
~cpp // 명시적인 반복문을 쓴다. vector<Object> a,b; b.clear(); for(vector<Object>::const_itarator VWCI = a.begin() + a.size()/2 ; VWCI != a.end() ; ++VWCI) b.push_back(*VWCI)
- 약간 익숙한 사람이라면..
~cpp // copy 알고리즘을 이용한다. vector<Object> a,b; b.clear(); copy( a.begin() + a.size() / 2, a.end(), back_inserter(b)) ;
- 어느 정도 알고, 익숙한 사람이면 다음과 같이..
~cpp // assign 메소드를 사용한다. vector<Object> a,b; b.assign(a.begin() + a.size() / 2, a.end());
- 이 네 가지 방법을 보자. 첫번째 두번째 방법은 루프를 사용한다. 두번째 방법에 루프가 어딨냐고 물으면 나는 모른다. copy 알고리즘 내부에서 루프를 사용한단다. 하지만 assign 메소드는 루프를 사용하지 않고 한번에 짠! 해주는거 같다.
- copy, push_back 이런것은 넣어줄때 insert iterator를 사용한다. 즉, 하나 넣고 메모리 할당 해주고, 객체 복사하고(큰 객체면... --; 묵념), 또 하나 넣어주고 저 짓하고.. 이런것이다. 하지만 assign은 똑같은 작업을 한번에 짠~, 만약 100개의 객체를 넣는다면 assign은 copy이런것보다 1/100 밖에 시간이 안걸린다는 것이다.(정확하진 않겠지만.. 뭐 그러하다.)
- 또 하나의 문제점, insert 메소드는 실행할때마다 새로운 공간을 할당하기 위해 하나씩 밀린다. 만약 컨테이너가 n개의 객체를 가지고 있고, 거기다 m개의 객체를 insert로 넣으면.. n*m만큼 뒤로 땡기느라 시간을 낭비하게 된다. int같은 기본 자료형이면 괜찮을지도 모르지만.. 만약에 객체가 큰 경우라면, 대입 연산자, 복사 생성자 이런것도 저만큼 호출하게 된다. 미친다.
- range 멤버 메소드는 주어진 두개의 반복자로 크기를 계산해서 한번에 옮기고 넣는다. 벡터를 예로 들면, 만약에 주어진 공간이 꽉 찼을때, insert를 수행하면, 새로운 공간 할당해서 지금의 내용들과, 넣으려는 것들을 그리로 옮기고 지금 있는걸 지우는 작업이 수행된다. 이짓을 100번 해보라, 컴퓨터가 상당히 기분나빠할지도 모른다. 하지만 range 멤버 메소드는 미리 늘려야 할 크기를 알기 때문에 한번만 하면 된다.
- 성능 따지기 골치 아픈 사람이라도, range 시리즈가 하나씩 시리즈보다 타이핑 양이 적다는 걸 알것이다.
6.1. 문제점? ¶
- 아직까지도 STL을 완벽하게 지원하는 컴파일러는 존재하지 않는다.
- 잊어버리고 있었던 클래스의 생성자에 관한..
~cpp class Widget {...}; // 디폴트 생성자가 있다고 가정 Widget a // 맞다. Widget b() // 과연?
- Widget b() 이것은 Widget형의 객체를 리턴해주는 b라는 이름을 가진 함수다. 이것과 관련해 파싱 에러가 자주 일어난다고 한다.
6.2. 예제 : ints.dat 에 저장되어 있는 정수들을 읽어서 list<int> data에 쓰는 작업을 한다. ¶
~cpp ifstream dataFile("ints.dat"); list<int> data(ifstream_iterator<int>(dataFile),ifstream_iterator<int>()); // 이런 방법도 있군. 난 맨날 돌려가면서 넣었는데..--;
- 왠지는 모르겠지만 두번째 문장은 작동하지 않는다. 위에서 제기한 문제 때문(괄호에 관련된)이다. 별루 중요한 것 같진 않으니 그냥 넘어감. 그래도 해결책은..
6.3. 해결책 ¶
~cpp ifstream dataFile("ints.dat"); ifstream_iterator<int> dataBegin(dataFile); ifstream_iterator<int> dataEnd; list<int> data(dataBegin, dataEnd); // 요런식으로 써주자.
7. Item7. When using containers of newed pointers, remember to delete the pointers before the container is destroyed ¶
7.1. 서론 ¶
- 누누히 강조했던 new로 만든거 넣었으면, delete 해주자는 것이다.
- 컨테이너가 파괴될때 포인터는 지워주겠지만, 포인터가 가리키는 객체는 destroy되지 않는다.(Detected Memory Leaks!)
7.2. 예제 ¶
~cpp vector<Object*> v; for(int i = 0 ; i < 10 ; ++i) v.push_back(new Object); // new로 넣어줬다. ... ... for(vector<Object*>::iterator i = v.begin() ; i != v.end() ; ++i) delete *i // 지워주자. return 0;
- 하지만 ... 부분에서 예외가 터져서 프로그램이 끝나버린다면? 또다시 Detected Memory Leaks!
7.3. 보완법 1(class for function object) ¶
- 이를 보완하기 위해 delete를 함수 객체로 만드는 법이 있다.(뭐야 이거?)
- Fucntion Object 보통 class키워드를 쓰던데, struct(이하 class라고 씀)를 써놨군. 뭐, 별 상관없지만, 내부 인자 없이 함수만으로 구성된 class이다. STL에서 Generic 을 구현하기에 주효한 방법이고, 함수로만 구성되어 있어서, 컴파일시에 전부 inline시킬수 있기 때문에 최적화 문제를 해결했음. 오 부지런히 보는가 보네. 나의 경쟁심을 자극 시키는 ^^;; --상민
- Fucntion Object 보통 class키워드를 쓰던데, struct(이하 class라고 씀)를 써놨군. 뭐, 별 상관없지만, 내부 인자 없이 함수만으로 구성된 class이다. STL에서 Generic 을 구현하기에 주효한 방법이고, 함수로만 구성되어 있어서, 컴파일시에 전부 inline시킬수 있기 때문에 최적화 문제를 해결했음. 오 부지런히 보는가 보네. 나의 경쟁심을 자극 시키는 ^^;; --상민
~cpp struct DeleteObject : public unary_function<const T*, void> { template<typename T> void operator()(const T* ptr) const { delete PTR; } }; void f() { ... for_each(v.begin(), v.End(), DeleteObject()); // 정말 이상하군..--; }
- 밑에꺼 쓰고 덧붙이는 건데 이 방법은 열라 안좋은 거란다.(struct DeleteObject, 쳇 그러면서 뭐하러 설명해?-.-)
7.4. 보완법 2(Smart Pointer) ¶
- 또 다른 방법으로 컨테이너에 포인터를 넣는 대신 스마트 포인터를 넣는 것이다.(아악..--; MDC++(MEC++)을 봐야 하는가..ㅠ.ㅠ)
~cpp typedef boost::shared_ptr<Object> SPO; // boost 라이브러리라는게 있단다. vector<SPO> v; for(int i = 0 ; i < 10 ; ++i) v.push_back(SPO(new Object));
8. Item8. Never create containers of auto_ptrs. ¶
- 생략. 도무지 뭔말하는건지 모르겠다. COAP는 또 뭐고..--;
- 컨테이너를 오토 포인터로 생성하지 말것~
- 컨테이너를 오토 포인터로 생성하지 말것~
9.1. 어떤 컨테이너가 int값들을 담고 있다고 하자. 거기서 1982 라는 숫자를 몽땅 지워주고 싶다면? ¶
- Contiguous-memory container 일때
~cpp c.erase( remove(c.begin(), c.end(), 1982), c.end() ); // 이건 내부적으로 어떻게 돌아가는 걸까. 찾아봐야겠군.
- list일때 - erase 써도 되지만 remove가 더 효율적이다.
~cpp c.remove(1982);
- Associative container 일때 - remove쓰면 난리난다.(없으니깐--;) 또 제네릭 알고리즘 remove도 역시 안된다. 컨테이너가 망가질수도 있다.
~cpp c.erase(1982);
9.2. 조건 검사해서 몽땅 지워주기 ¶
- 또 이상한 팁이 있군. bool형을 리턴하는 함수를 인자로 같이 넣어주면 그 조건을 만족하는 값은 모조리 지워주는..(함수 포인터일까.. 함수 포인터는 아직도 언제 쓰는건지 모르겠다. 혹시 아시는분은 좀 가르쳐 주세요^^;)
- 함수포인터임. (또는 위에서의 함수객체.) 해당 함수 객체로 조건을 파악한뒤 삭제하거나 비교,소트 하는 일을 많이 함. --1002
- 함수포인터임. (또는 위에서의 함수객체.) 해당 함수 객체로 조건을 파악한뒤 삭제하거나 비교,소트 하는 일을 많이 함. --1002
9.2.1. 쉽게 할수 있는 컨테이너들 ¶
~cpp bool badValue(int x) { ... } // 넣어줄 함수 선언
~cpp c.erase( remove_if(c.begin(), c.end(), badValue), c.end() ); // Contiguous-memory Container일때(vector, deque, string)
~cpp c.remove_if(badValue); // list일때
9.2.2. Associative Container 일때 ¶
- 쉽지만 비효율적인 방법 - remove_copy_if 를 사용한다. 이것은 지우고 싶은걸 제외한 나머지 것들을 새로운 컨테이너로 옮긴다. 그 다음에 원래 컨테이너를 새로 만든것과 바꿔 버린다.(우울하군--;)
~cpp AssocContainer<int> c; // map,multimap,set,multiset 등을 일반화한 이름이다. 실제로 저런 이름의 컨테이너는 없다.--; ... AssocContainer<int> goodValues; remove_copy_if(c.begin(), c.end(), inserter(goodValues, goodValues.end()), badValue); // 헉 이분법--;. 보면서 느끼는 거지만 정말 신기한거 많다. 저런 것들을 도대체 무슨 생각으로 구현했을지.. c.swap(goodValues); // c랑 goodValues랑 바꾼다. 이런것도 있군.
- 어렵지만 효율적인 방법 - 뭐 또 파일에 쓴다고 해서 이상한방법 쓰는데 그냥 넘겼다.
~cpp AssocContainer<int> c; for(AssocContainer<int>::iterator i = c.begin() ; c != c.end() ; ) { if(badValue(*i)) { c.erase(i++); // 지워야 할 값이면 일단 지우고 반복자 하나 증가시켜준다. 후위 연산자는 그 값을 미리 복사를 하기 떄문에 가능한 일이다. } // 그냥 지우면 그 반복자는 void가 된다. 안좋다--; else ++i; // 아니면 그냥 증가 }